From 10562e72f4340464f113feb45ce18d0f794fa45f Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 8 Oct 2024 08:12:42 -0700 Subject: [PATCH 001/177] Initial update to new renderer --- packages/mermaid/src/config.type.ts | 2 + packages/mermaid/src/diagrams/er/erDb.js | 103 -------- packages/mermaid/src/diagrams/er/erDb.ts | 240 ++++++++++++++++++ packages/mermaid/src/diagrams/er/erDiagram.ts | 2 +- .../src/diagrams/er/erRenderer-unified.ts | 46 ++++ packages/mermaid/src/diagrams/er/erTypes.ts | 37 +++ packages/mermaid/src/diagrams/er/styles.js | 33 ++- .../mermaid/src/schemas/config.schema.yaml | 8 + 8 files changed, 364 insertions(+), 107 deletions(-) delete mode 100644 packages/mermaid/src/diagrams/er/erDb.js create mode 100644 packages/mermaid/src/diagrams/er/erDb.ts create mode 100644 packages/mermaid/src/diagrams/er/erRenderer-unified.ts create mode 100644 packages/mermaid/src/diagrams/er/erTypes.ts diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 035a158e0..cd8401d7c 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -793,6 +793,8 @@ export interface ErDiagramConfig extends BaseDiagramConfig { * */ entityPadding?: number; + nodeSpacing?: number; + rankSpacing?: number; /** * Stroke color of box edges and lines. */ diff --git a/packages/mermaid/src/diagrams/er/erDb.js b/packages/mermaid/src/diagrams/er/erDb.js deleted file mode 100644 index f24f48198..000000000 --- a/packages/mermaid/src/diagrams/er/erDb.js +++ /dev/null @@ -1,103 +0,0 @@ -import { log } from '../../logger.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; - -import { - setAccTitle, - getAccTitle, - getAccDescription, - setAccDescription, - clear as commonClear, - setDiagramTitle, - getDiagramTitle, -} from '../common/commonDb.js'; - -let entities = new Map(); -let relationships = []; - -const Cardinality = { - ZERO_OR_ONE: 'ZERO_OR_ONE', - ZERO_OR_MORE: 'ZERO_OR_MORE', - ONE_OR_MORE: 'ONE_OR_MORE', - ONLY_ONE: 'ONLY_ONE', - MD_PARENT: 'MD_PARENT', -}; - -const Identification = { - NON_IDENTIFYING: 'NON_IDENTIFYING', - IDENTIFYING: 'IDENTIFYING', -}; -/** - * Add entity - * @param {string} name - The name of the entity - * @param {string | undefined} alias - The alias of the entity - */ -const addEntity = function (name, alias = undefined) { - if (!entities.has(name)) { - entities.set(name, { attributes: [], alias }); - log.info('Added new entity :', name); - } else if (!entities.get(name).alias && alias) { - entities.get(name).alias = alias; - log.info(`Add alias '${alias}' to entity '${name}'`); - } - - return entities.get(name); -}; - -const getEntities = () => entities; - -const addAttributes = function (entityName, attribs) { - let entity = addEntity(entityName); // May do nothing (if entity has already been added) - - // Process attribs in reverse order due to effect of recursive construction (last attribute is first) - let i; - for (i = attribs.length - 1; i >= 0; i--) { - entity.attributes.push(attribs[i]); - log.debug('Added attribute ', attribs[i].attributeName); - } -}; - -/** - * Add a relationship - * - * @param entA The first entity in the relationship - * @param rolA The role played by the first entity in relation to the second - * @param entB The second entity in the relationship - * @param rSpec The details of the relationship between the two entities - */ -const addRelationship = function (entA, rolA, entB, rSpec) { - let rel = { - entityA: entA, - roleA: rolA, - entityB: entB, - relSpec: rSpec, - }; - - relationships.push(rel); - log.debug('Added new relationship :', rel); -}; - -const getRelationships = () => relationships; - -const clear = function () { - entities = new Map(); - relationships = []; - commonClear(); -}; - -export default { - Cardinality, - Identification, - getConfig: () => getConfig().er, - addEntity, - addAttributes, - getEntities, - addRelationship, - getRelationships, - clear, - setAccTitle, - getAccTitle, - setAccDescription, - getAccDescription, - setDiagramTitle, - getDiagramTitle, -}; diff --git a/packages/mermaid/src/diagrams/er/erDb.ts b/packages/mermaid/src/diagrams/er/erDb.ts new file mode 100644 index 000000000..c8ea3136c --- /dev/null +++ b/packages/mermaid/src/diagrams/er/erDb.ts @@ -0,0 +1,240 @@ +import { log } from '../../logger.js'; +import { getConfig } from '../../diagram-api/diagramAPI.js'; +import type { Edge, Node } from '../../rendering-util/types.js'; +import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js'; + +import { + setAccTitle, + getAccTitle, + getAccDescription, + setAccDescription, + clear as commonClear, + setDiagramTitle, + getDiagramTitle, +} from '../common/commonDb.js'; +import { getEdgeId } from '../../utils.js'; + +let entities = new Map(); +let relationships: Relationship[] = []; +let classes = new Map(); +let direction = 'TB'; + +const Cardinality = { + ZERO_OR_ONE: 'ZERO_OR_ONE', + ZERO_OR_MORE: 'ZERO_OR_MORE', + ONE_OR_MORE: 'ONE_OR_MORE', + ONLY_ONE: 'ONLY_ONE', + MD_PARENT: 'MD_PARENT', +}; + +const Identification = { + NON_IDENTIFYING: 'NON_IDENTIFYING', + IDENTIFYING: 'IDENTIFYING', +}; +/** + * Add entity + * @param name - The name of the entity + * @param alias - The alias of the entity + */ +const addEntity = function (name: string, alias = ''): EntityNode { + if (!entities.has(name)) { + entities.set(name, { + id: `entity-${name}-${entities.size}`, + label: name, + attributes: [], + alias, + shape: 'erBox', + look: getConfig().look || 'default', + cssClasses: [], + cssStyles: [], + }); + log.info('Added new entity :', name); + } else if (!entities.get(name)?.alias && alias) { + entities.get(name)!.alias = alias; + log.info(`Add alias '${alias}' to entity '${name}'`); + } + + return entities.get(name)!; +}; + +const getEntity = function (name: string) { + return entities.get(name); +}; + +const getEntities = () => entities; + +const getClasses = () => classes; + +const addAttributes = function (entityName: string, attribs: Attribute[]) { + const entity = addEntity(entityName); // May do nothing (if entity has already been added) + + // Process attribs in reverse order due to effect of recursive construction (last attribute is first) + let i; + for (i = attribs.length - 1; i >= 0; i--) { + if (!attribs[i].keys) { + attribs[i].keys = []; + } + if (!attribs[i].comment) { + attribs[i].comment = ''; + } + entity.attributes.push(attribs[i]); + log.debug('Added attribute ', attribs[i].name); + } +}; + +/** + * Add a relationship + * + * @param entA - The first entity in the relationship + * @param rolA - The role played by the first entity in relation to the second + * @param entB - The second entity in the relationship + * @param rSpec - The details of the relationship between the two entities + */ +const addRelationship = function (entA: string, rolA: string, entB: string, rSpec: RelSpec) { + const entityA = entities.get(entA); + const entityB = entities.get(entB); + if (!entityA || !entityB) { + return; + } + + const rel = { + entityA: entityA.id, + roleA: rolA, + entityB: entityB.id, + relSpec: rSpec, + }; + + relationships.push(rel); + log.debug('Added new relationship :', rel); +}; + +const getRelationships = () => relationships; + +const getDirection = () => direction; +const setDirection = (dir: string) => { + direction = dir; +}; + +const clear = function () { + entities = new Map(); + classes = new Map(); + relationships = []; + commonClear(); +}; + +export const getData = function () { + const nodes: Node[] = []; + const edges: Edge[] = []; + const config = getConfig(); + + for (const entityKey of entities.keys()) { + const entityNode = entities.get(entityKey); + if (entityNode) { + entityNode.cssCompiledStyles = getCompiledStyles(entityNode.cssClasses!); + nodes.push(entityNode as unknown as Node); + } + } + + let cnt = 0; + for (const relationship of relationships) { + const edge: Edge = { + id: getEdgeId(relationship.entityA, relationship.entityB, { prefix: 'id', counter: cnt++ }), + type: 'normal', + start: relationship.entityA, + end: relationship.entityB, + label: relationship.roleA, + labelpos: 'c', + thickness: 'normal', + classes: 'relationshipLine', + arrowTypeStart: relationship.relSpec.cardB.toLowerCase(), + arrowTypeEnd: relationship.relSpec.cardA.toLowerCase(), + pattern: relationship.relSpec.relType == 'IDENTIFYING' ? 'solid' : 'dashed', + look: config.look, + }; + edges.push(edge); + } + return { nodes, edges, other: {}, config, direction: 'TB' }; +}; + +export const addCssStyles = function (ids: string[], styles: string[]) { + for (const id of ids) { + const entity = entities.get(id); + if (!styles || !entity) { + return; + } + for (const style of styles) { + entity.cssStyles!.push(style); + } + } +}; + +export const addClass = function (ids: string[], style: string[]) { + ids.forEach(function (id) { + let classNode = classes.get(id); + if (classNode === undefined) { + classNode = { id, styles: [], textStyles: [] }; + classes.set(id, classNode); + } + + if (style) { + style.forEach(function (s) { + if (/color/.exec(s)) { + const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill'); + classNode.textStyles.push(newStyle); + } + classNode.styles.push(s); + }); + } + }); +}; + +export const setClass = function (ids: string[], classNames: string[]) { + for (const id of ids) { + const entity = entities.get(id); + if (entity) { + for (const className of classNames) { + entity.cssClasses!.push(className); + } + } + } +}; + +function getCompiledStyles(classDefs: string[]) { + let compiledStyles: string[] = []; + for (const customClass of classDefs) { + const cssClass = classes.get(customClass); + if (cssClass?.styles) { + compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim()); + } + if (cssClass?.textStyles) { + compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim()); + } + } + return compiledStyles; +} + +export default { + Cardinality, + Identification, + getConfig: () => getConfig().er, + addEntity, + addAttributes, + getEntities, + getEntity, + getClasses, + addRelationship, + getRelationships, + clear, + getDirection, + setDirection, + setAccTitle, + getAccTitle, + setAccDescription, + getAccDescription, + setDiagramTitle, + getDiagramTitle, + getData, + addCssStyles, + addClass, + setClass, +}; diff --git a/packages/mermaid/src/diagrams/er/erDiagram.ts b/packages/mermaid/src/diagrams/er/erDiagram.ts index adfa525fc..1647f181b 100644 --- a/packages/mermaid/src/diagrams/er/erDiagram.ts +++ b/packages/mermaid/src/diagrams/er/erDiagram.ts @@ -1,7 +1,7 @@ // @ts-ignore: TODO: Fix ts errors import erParser from './parser/erDiagram.jison'; import erDb from './erDb.js'; -import erRenderer from './erRenderer.js'; +import erRenderer from './erRenderer-unified.js'; import erStyles from './styles.js'; export const diagram = { diff --git a/packages/mermaid/src/diagrams/er/erRenderer-unified.ts b/packages/mermaid/src/diagrams/er/erRenderer-unified.ts new file mode 100644 index 000000000..be668df69 --- /dev/null +++ b/packages/mermaid/src/diagrams/er/erRenderer-unified.ts @@ -0,0 +1,46 @@ +import { getConfig } from '../../diagram-api/diagramAPI.js'; +import { log } from '../../logger.js'; +import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js'; +import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; +import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; +import type { LayoutData } from '../../rendering-util/types.js'; +import db from './erDb.js'; +import utils from '../../utils.js'; + +export const draw = async function (text: string, id: string, _version: string, diag: any) { + log.info('REF0:'); + log.info('Drawing er diagram (unified)', id); + const { securityLevel, er: conf, layout } = getConfig(); + + // The getData method provided in all supported diagrams is used to extract the data from the parsed structure + // into the Layout data format + const data4Layout = diag.db.getData() as LayoutData; + + // Create the root SVG - the element is the div containing the SVG element + const svg = getDiagramElement(id, securityLevel); + + data4Layout.type = diag.type; + data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); + + // Workaround as when rendering and setting up the graph it uses flowchart spacing before data4Layout spacing? + data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140; + data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80; + data4Layout.direction = db.getDirection(); + + data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more']; + data4Layout.diagramId = id; + await render(data4Layout, svg); + const padding = 8; + utils.insertTitle( + svg, + 'erDiagramTitleText', + conf?.titleTopMargin ?? 25, + diag.db.getDiagramTitle() + ); + + setupViewPortForSVG(svg, padding, 'erDiagram', conf?.useMaxWidth ?? true); +}; + +export default { + draw, +}; diff --git a/packages/mermaid/src/diagrams/er/erTypes.ts b/packages/mermaid/src/diagrams/er/erTypes.ts new file mode 100644 index 000000000..7670302f0 --- /dev/null +++ b/packages/mermaid/src/diagrams/er/erTypes.ts @@ -0,0 +1,37 @@ +export interface EntityNode { + id: string; + label: string; + attributes: Attribute[]; + alias: string; + shape: string; + look?: string; + cssClasses?: string[]; + cssStyles?: string[]; + cssCompiledStyles?: string[]; +} + +export interface Attribute { + type: string; + name: string; + keys: ('PK' | 'FK' | 'UK')[]; + comment: string; +} + +export interface Relationship { + entityA: string; + roleA: string; + entityB: string; + relSpec: RelSpec; +} + +export interface RelSpec { + cardA: string; + cardB: string; + relType: string; +} + +export interface EntityClass { + id: string; + styles: string[]; + textStyles: string[]; +} diff --git a/packages/mermaid/src/diagrams/er/styles.js b/packages/mermaid/src/diagrams/er/styles.js index 08ea2e851..45a9dd7ec 100644 --- a/packages/mermaid/src/diagrams/er/styles.js +++ b/packages/mermaid/src/diagrams/er/styles.js @@ -24,9 +24,36 @@ const getStyles = (options) => } } - .relationshipLine { - stroke: ${options.lineColor}; - } + .edgeLabel .label { + fill: ${options.nodeBorder}; + font-size: 14px; + } + + .edgeLabel .label .labelBkg { + background: ${options.mainBkg}; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${options.mainBkg}; + stroke: ${options.nodeBorder}; + stroke-width: 1px; + } + + .relationshipLine { + stroke: ${options.lineColor}; + stroke-width: 1; + fill: none; + } + + .marker { + fill: none !important; + stroke: ${options.lineColor} !important; + stroke-width: 1; + } .entityTitleText { text-anchor: middle; diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index a7b3549eb..980561084 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -1280,6 +1280,14 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) type: integer default: 15 minimum: 0 + nodeSpacing: + type: integer + default: 140 + minimum: 0 + rankSpacing: + type: integer + default: 80 + minimum: 0 stroke: description: Stroke color of box edges and lines. type: string From f2f1ad46055de83607b9c6683a353e87d68028e8 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 8 Oct 2024 08:22:34 -0700 Subject: [PATCH 002/177] Add erBox shape --- .../rendering-elements/shapes/erBox.ts | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts new file mode 100644 index 000000000..2c812b862 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts @@ -0,0 +1,319 @@ +import { updateNodeBounds, getNodeClasses } from './util.js'; +import intersect from '../intersect/index.js'; +import type { Node } from '../../types.js'; +import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js'; +import rough from 'roughjs'; +import { drawRect } from './drawRect.js'; +import { getConfig } from '../../../config.js'; +import type { EntityNode } from '../../../diagrams/er/erTypes.js'; +import { createText } from '../../createText.js'; +import { evaluate, parseGenericTypes } from '../../../diagrams/common/common.js'; +import { select } from 'd3'; +import { calculateTextWidth } from '../../../utils.js'; +import type { MermaidConfig } from '../../../config.type.js'; + +export const erBox = async (parent: SVGAElement, node: Node) => { + // Treat node as entityNode for certain entityNode checks + const entityNode = node as unknown as EntityNode; + if (entityNode.alias) { + node.label = entityNode.alias; + } + const config = getConfig(); + node.useHtmlLabels = config.htmlLabels; + let PADDING = config.er?.diagramPadding ?? 10; + let TEXT_PADDING = config.er?.entityPadding ?? 6; + + const { cssStyles } = node; + const { labelStyles } = styles2String(node); + + // Draw rect if no attributes are found + if (entityNode.attributes.length === 0 && node.label) { + const options = { + rx: 0, + ry: 0, + labelPaddingX: PADDING, + labelPaddingY: PADDING * 1.5, + classes: '', + }; + // Set minimum width + if ( + calculateTextWidth(node.label, config) + options.labelPaddingX * 2 < + config.er!.minEntityWidth! + ) { + node.width = config.er!.minEntityWidth; + } + const shapeSvg = await drawRect(parent, node, options); + return shapeSvg; + } + + if (!config.htmlLabels) { + PADDING *= 1.25; + TEXT_PADDING *= 1.25; + } + + let cssClasses = getNodeClasses(node); + if (!cssClasses) { + cssClasses = 'node default'; + } + + const shapeSvg = parent + // @ts-ignore Ignore .insert on SVGAElement + .insert('g') + .attr('class', cssClasses) + .attr('id', node.domId || node.id); + + // TODO: Make padding better + + const nameBBox = await addText(shapeSvg, node.label ?? '', config, 0, 0, ['name'], labelStyles); + nameBBox.height += TEXT_PADDING; + let yOffset = 0; + const yOffsets = []; + let maxTypeWidth = 0; + let maxNameWidth = 0; + let maxKeysWidth = 0; + let maxCommentWidth = 0; + let keysPresent = true; + let commentPresent = true; + for (const attribute of entityNode.attributes) { + const typeBBox = await addText( + shapeSvg, + attribute.type, + config, + 0, + yOffset, + ['attribute-type'], + labelStyles + ); + maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width + PADDING); + const nameBBox = await addText( + shapeSvg, + attribute.name, + config, + 0, + yOffset, + ['attribute-name'], + labelStyles + ); + maxNameWidth = Math.max(maxNameWidth, nameBBox.width + PADDING); + const keysBBox = await addText( + shapeSvg, + attribute.keys.join(), + config, + 0, + yOffset, + ['attribute-keys'], + labelStyles + ); + maxKeysWidth = Math.max(maxKeysWidth, keysBBox.width + PADDING); + const commentBBox = await addText( + shapeSvg, + attribute.comment, + config, + 0, + yOffset, + ['attribute-comment'], + labelStyles + ); + maxCommentWidth = Math.max(maxCommentWidth, commentBBox.width + PADDING); + + yOffset += + Math.max(typeBBox.height, nameBBox.height, keysBBox.height, commentBBox.height) + + TEXT_PADDING; + yOffsets.push(yOffset); + } + + yOffsets.pop(); + let totalWidthSections = 4; + + if (maxKeysWidth <= PADDING) { + keysPresent = false; + maxKeysWidth = 0; + totalWidthSections--; + } + if (maxCommentWidth <= PADDING) { + commentPresent = false; + maxCommentWidth = 0; + totalWidthSections--; + } + + const shapeBBox = shapeSvg.node().getBBox(); + // Add extra padding to attribute components to accommodate for difference in width + if ( + nameBBox.width + PADDING * 2 - (maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth) > + 0 + ) { + const difference = + nameBBox.width + PADDING * 2 - (maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth); + maxTypeWidth += difference / totalWidthSections; + maxNameWidth += difference / totalWidthSections; + if (maxKeysWidth > 0) { + maxKeysWidth += difference / totalWidthSections; + } + if (maxCommentWidth > 0) { + maxCommentWidth += difference / totalWidthSections; + } + } + + const maxWidth = maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth; + + // @ts-ignore TODO: Fix rough typings + const rc = rough.svg(shapeSvg); + const options = userNodeOverrides(node, {}); + + if (node.look !== 'handDrawn') { + options.roughness = 0; + options.fillStyle = 'solid'; + } + + const w = Math.max(shapeBBox.width + PADDING * 2, node?.width || 0, maxWidth); + const h = Math.max(shapeBBox.height + (yOffsets[0] || yOffset) + TEXT_PADDING, node?.height || 0); + const x = -w / 2; + const y = -h / 2; + + // Translate attribute text labels + shapeSvg.selectAll('g:not(:first-child)').each((_: any, i: number, nodes: any) => { + const text = select(nodes[i]); + const transform = text.attr('transform'); + let translateX = 0; + let translateY = 0; + + if (transform) { + const regex = RegExp(/translate\(([^,]+),([^)]+)\)/); + const translate = regex.exec(transform); + if (translate) { + translateX = parseFloat(translate[1]); + translateY = parseFloat(translate[2]); + if (text.attr('class').includes('attribute-name')) { + translateX += maxTypeWidth; + } else if (text.attr('class').includes('attribute-keys')) { + translateX += maxTypeWidth + maxNameWidth; + } else if (text.attr('class').includes('attribute-comment')) { + translateX += maxTypeWidth + maxNameWidth + maxKeysWidth; + } + } + } + + text.attr( + 'transform', + `translate(${x + PADDING / 2 + translateX}, ${translateY + y + nameBBox.height + TEXT_PADDING / 2})` + ); + }); + // Center the name + shapeSvg + .select('.name') + .attr('transform', 'translate(' + -nameBBox.width / 2 + ', ' + (y + TEXT_PADDING / 2) + ')'); + + // Draw rect + const roughRect = rc.rectangle(x, y, w, h, options); + const rect = shapeSvg.insert(() => roughRect, ':first-child').attr('style', cssStyles); + + // Draw divider lines + // Name line + let roughLine = rc.line(x, nameBBox.height + y, w + x, nameBBox.height + y, options); + shapeSvg.insert(() => roughLine).attr('class', 'divider'); + // First line + roughLine = rc.line(maxTypeWidth + x, nameBBox.height + y, maxTypeWidth + x, h + y, options); + shapeSvg.insert(() => roughLine).attr('class', 'divider'); + // Second line + if (keysPresent) { + roughLine = rc.line( + maxTypeWidth + maxNameWidth + x, + nameBBox.height + y, + maxTypeWidth + maxNameWidth + x, + h + y, + options + ); + shapeSvg.insert(() => roughLine).attr('class', 'divider'); + } + // Third line + if (commentPresent) { + roughLine = rc.line( + maxTypeWidth + maxNameWidth + maxKeysWidth + x, + nameBBox.height + y, + maxTypeWidth + maxNameWidth + maxKeysWidth + x, + h + y, + options + ); + shapeSvg.insert(() => roughLine).attr('class', 'divider'); + } + + // Attribute divider lines + for (const yOffset of yOffsets) { + roughLine = rc.line( + x, + nameBBox.height + y + yOffset, + w + x, + nameBBox.height + y + yOffset, + options + ); + shapeSvg.insert(() => roughLine).attr('class', 'divider'); + } + + updateNodeBounds(node, rect); + + node.intersect = function (point) { + return intersect.rect(node, point); + }; + return shapeSvg; +}; + +// Helper function to add label text g with translate position and style +async function addText( + shapeSvg: any, + labelText: string, + config: MermaidConfig, + translateX = 0, + translateY = 0, + classes: string[] = [], + style = '' +) { + const label = shapeSvg + .insert('g') + .attr('class', `label ${classes.join(' ')}`) + .attr('transform', `translate(${translateX}, ${translateY})`) + .attr('style', style); + + // Return types need to be parsed + if (labelText !== parseGenericTypes(labelText)) { + labelText = parseGenericTypes(labelText); + // Work around + labelText = labelText.replaceAll('<', '<').replaceAll('>', '>'); + } + + const text = label + .node() + .appendChild( + await createText( + label, + labelText, + { + width: calculateTextWidth(labelText, config) + 100, + style, + useHtmlLabels: config.htmlLabels, + }, + config + ) + ); + // Undo work around now that text passed through correctly + if (labelText.includes('<')) { + let child = text.children[0]; + // Get last child + while (child.childNodes[0]) { + child = child.childNodes[0]; + } + // Replace its text content + child.textContent = child.textContent.replaceAll('<', '<').replaceAll('>', '>'); + } + + let bbox = text.getBBox(); + if (evaluate(config.htmlLabels)) { + const div = text.children[0]; + div.style.textAlign = 'start'; + const dv = select(text); + bbox = div.getBoundingClientRect(); + dv.attr('width', bbox.width); + dv.attr('height', bbox.height); + } + + return bbox; +} From 983c28686f051a573d313c87769c8430fb9271a4 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 8 Oct 2024 08:23:51 -0700 Subject: [PATCH 003/177] Register erBox shape in nodes --- packages/mermaid/src/rendering-util/rendering-elements/nodes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js index 2f69a36bc..1376caaae 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/nodes.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/nodes.js @@ -22,6 +22,7 @@ import { lean_left } from './shapes/leanLeft.js'; import { trapezoid } from './shapes/trapezoid.js'; import { inv_trapezoid } from './shapes/invertedTrapezoid.js'; import { labelRect } from './shapes/labelRect.js'; +import { erBox } from './shapes/erBox.js'; const shapes = { state, @@ -47,6 +48,7 @@ const shapes = { trapezoid, inv_trapezoid, labelRect, + erBox, }; const nodeElems = new Map(); From d2892ea2490a124bfb3ac97ec799fe63f430dafe Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 8 Oct 2024 08:24:41 -0700 Subject: [PATCH 004/177] Add erDiagram specific arrow markers --- .../rendering-elements/edgeMarker.ts | 4 + .../rendering-elements/markers.js | 126 ++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts index 5371ac32d..8da8ba95d 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts @@ -35,6 +35,10 @@ const arrowTypesMap = { composition: 'composition', dependency: 'dependency', lollipop: 'lollipop', + only_one: 'onlyOne', + zero_or_one: 'zeroOrOne', + one_or_more: 'oneOrMore', + zero_or_more: 'zeroOrMore', } as const; const addEdgeMarker = ( diff --git a/packages/mermaid/src/rendering-util/rendering-elements/markers.js b/packages/mermaid/src/rendering-util/rendering-elements/markers.js index b2592e20e..0830cfe8b 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/markers.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/markers.js @@ -277,6 +277,128 @@ const barb = (elem, type, id) => { .append('path') .attr('d', 'M 19,7 L9,13 L14,7 L9,1 Z'); }; +// erDiagram specific markers +const only_one = (elem, type, id) => { + elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-onlyOneStart') + .attr('class', 'marker onlyOne ' + type) + .attr('refX', 0) + .attr('refY', 9) + .attr('markerWidth', 18) + .attr('markerHeight', 18) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M9,0 L9,18 M15,0 L15,18'); + + elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-onlyOneEnd') + .attr('class', 'marker onlyOne ' + type) + .attr('refX', 18) + .attr('refY', 9) + .attr('markerWidth', 18) + .attr('markerHeight', 18) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M3,0 L3,18 M9,0 L9,18'); +}; + +const zero_or_one = (elem, type, id) => { + const startMarker = elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-zeroOrOneStart') + .attr('class', 'marker zeroOrOne ' + type) + .attr('refX', 0) + .attr('refY', 9) + .attr('markerWidth', 30) + .attr('markerHeight', 18) + .attr('orient', 'auto'); + startMarker + .append('circle') + .attr('fill', 'white') // Fill white for now? + .attr('cx', 21) + .attr('cy', 9) + .attr('r', 6); + startMarker.append('path').attr('d', 'M9,0 L9,18'); + + const endMarker = elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-zeroOrOneEnd') + .attr('class', 'marker zeroOrOne ' + type) + .attr('refX', 30) + .attr('refY', 9) + .attr('markerWidth', 30) + .attr('markerHeight', 18) + .attr('orient', 'auto'); + endMarker + .append('circle') + .attr('fill', 'white') // Fill white for now? + .attr('cx', 9) + .attr('cy', 9) + .attr('r', 6); + endMarker.append('path').attr('d', 'M21,0 L21,18'); +}; + +const one_or_more = (elem, type, id) => { + elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-oneOrMoreStart') + .attr('class', 'marker oneOrMore ' + type) + .attr('refX', 18) + .attr('refY', 18) + .attr('markerWidth', 45) + .attr('markerHeight', 36) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27'); + + elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-oneOrMoreEnd') + .attr('class', 'marker oneOrMore ' + type) + .attr('refX', 27) + .attr('refY', 18) + .attr('markerWidth', 45) + .attr('markerHeight', 36) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18'); +}; + +const zero_or_more = (elem, type, id) => { + const startMarker = elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-zeroOrMoreStart') + .attr('class', 'marker zeroOrMore ' + type) + .attr('refX', 18) + .attr('refY', 18) + .attr('markerWidth', 57) + .attr('markerHeight', 36) + .attr('orient', 'auto'); + startMarker.append('circle').attr('fill', 'white').attr('cx', 48).attr('cy', 18).attr('r', 6); + startMarker.append('path').attr('d', 'M0,18 Q18,0 36,18 Q18,36 0,18'); + + const endMarker = elem + .append('defs') + .append('marker') + .attr('id', id + '_' + type + '-zeroOrMoreEnd') + .attr('class', 'marker zeroOrMore ' + type) + .attr('refX', 39) + .attr('refY', 18) + .attr('markerWidth', 57) + .attr('markerHeight', 36) + .attr('orient', 'auto'); + endMarker.append('circle').attr('fill', 'white').attr('cx', 9).attr('cy', 18).attr('r', 6); + endMarker.append('path').attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18'); +}; // TODO rename the class diagram markers to something shape descriptive and semantic free const markers = { @@ -289,5 +411,9 @@ const markers = { circle, cross, barb, + only_one, + zero_or_one, + one_or_more, + zero_or_more, }; export default insertMarkers; From d0dd4e6e5e7f7f149423e51603891b24bc8817ee Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 8 Oct 2024 08:25:34 -0700 Subject: [PATCH 005/177] Fix cyclical / recursive edges to not show duplicated arrow start markers --- .../mermaid/src/rendering-util/layout-algorithms/dagre/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js index 9367a65a5..23461d9c5 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js +++ b/packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js @@ -346,6 +346,7 @@ export const render = async (data4Layout, svg) => { edge1.label = ''; edge1.arrowTypeEnd = 'none'; edge1.id = nodeId + '-cyclic-special-1'; + edgeMid.arrowTypeStart = 'none'; edgeMid.arrowTypeEnd = 'none'; edgeMid.id = nodeId + '-cyclic-special-mid'; edge2.label = ''; @@ -354,6 +355,7 @@ export const render = async (data4Layout, svg) => { edge2.toCluster = nodeId; } edge2.id = nodeId + '-cyclic-special-2'; + edge2.arrowTypeStart = 'none'; graph.setEdge(nodeId, specialId1, edge1, nodeId + '-cyclic-special-0'); graph.setEdge(specialId1, specialId2, edgeMid, nodeId + '-cyclic-special-1'); graph.setEdge(specialId2, nodeId, edge2, nodeId + '-cyc Date: Tue, 8 Oct 2024 08:26:12 -0700 Subject: [PATCH 006/177] Fix diagram title not being centered --- packages/mermaid/src/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index 631b6dd85..cc033528c 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -824,6 +824,7 @@ export const insertTitle = ( parent .append('text') .text(title) + .attr('text-anchor', 'middle') .attr('x', bounds.x + bounds.width / 2) .attr('y', -titleTopMargin) .attr('class', cssClass); From 6247bd5f895612710a6216d426af6286812ae638 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 8 Oct 2024 08:26:43 -0700 Subject: [PATCH 007/177] Add test html file --- cypress/platform/yari2.html | 174 ++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 cypress/platform/yari2.html diff --git a/cypress/platform/yari2.html b/cypress/platform/yari2.html new file mode 100644 index 000000000..35fa2493b --- /dev/null +++ b/cypress/platform/yari2.html @@ -0,0 +1,174 @@ + + +

Nodes

+
+
+

Basic ErNode

+
+          ---
+            config:
+              htmlLabels: false
+              look: handDrawn
+              theme: forest
+          ---
+          erDiagram
+            _**hiØ**_[*test*] {
+              *__List~List~int~~sdfds__* __driversLicense__ PK "***The l😀icense #***"
+              *string(99)~T~~~~~~* firstName "Only 99 
characters are a
llowed dsfsdfsdfsdfs" + string lastName + string phone UK + int age + } + style PERSON color:red, stroke:blue,fill:#f9f + classDef test,test2 stroke:red + class PERSON test,test2 +
+
+
+

Basic ErNode

+
+          ---
+            config:
+              htmlLabels: true
+              look: handDrawn
+          ---
+          erDiagram
+            c[CUST😀OMER]
+            p[__PERSON__]
+            style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold
+        
+
+
+

Diagram Testing

+
+
+

Basic Relationship

+
+          ---
+            config:
+              htmlLabels: false
+              look:
+          ---
+          erDiagram
+            "hi" }o..o{ ORDER : places
+        
+
+
+

Basic Relationship

+
+          ---
+            config:
+              htmlLabels: false
+              look:
+          ---
+          erDiagram
+            CAR ||--|{ NAMED-DRIVER : allows
+            PERSON ||..o{ NAMED-DRIVER : is
+        
+
+
+

Basic Relationship

+
+          ---
+            config:
+              htmlLabels: true
+          ---
+          erDiagram
+            CAR ||--o{ NAMED-DRIVER : allows
+            CAR {
+                test test PK "comment"
+                string make
+                string model
+                string[] parts
+            }
+            PERSON ||--o{ NAMED-DRIVER : is
+            PERSON ||--o{ CAR : is
+            PERSON {
+                string driversLicense PK "The license #"
+                string(99) firstName "Only 99 characters are allowed"
+                string lastName
+                string phone UK
+                int age
+            }
+            NAMED-DRIVER {
+                string carRegistrationNumber PK, FK
+                string driverLicence PK, FK
+            }
+            MANUFACTURER only one to zero or more CAR : makes      
+        
+
+
+

Basic Relationship

+
+          ---
+            title: simple ER diagram
+            config:
+              theme: forest
+          ---
+          erDiagram
+          direction TB
+          p[Pers😀on] {
+              string firstName
+              string lastName
+          }
+          a["Customer Account"] {
+              string email
+          }
+          p ||--o| a : has
+           
+        
+
+
+

Basic Relationship

+
+          erDiagram
+            CUSTOMER ||--o{ CUSTOMER : refers
+            CUSTOMER ||--o{ ORDER : places
+            ORDER ||--|{ LINE-ITEM : contains
+        
+
+
+ + + + + From df110e5e11bd38cf60249ede91287c8667749fa6 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 8 Oct 2024 11:47:08 -0700 Subject: [PATCH 008/177] Add default class to nodes and fix styles --- packages/mermaid/src/diagrams/er/erDb.ts | 2 +- packages/mermaid/src/diagrams/er/styles.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/er/erDb.ts b/packages/mermaid/src/diagrams/er/erDb.ts index c8ea3136c..ef0b5382d 100644 --- a/packages/mermaid/src/diagrams/er/erDb.ts +++ b/packages/mermaid/src/diagrams/er/erDb.ts @@ -45,7 +45,7 @@ const addEntity = function (name: string, alias = ''): EntityNode { alias, shape: 'erBox', look: getConfig().look || 'default', - cssClasses: [], + cssClasses: ['default'], cssStyles: [], }); log.info('Added new entity :', name); diff --git a/packages/mermaid/src/diagrams/er/styles.js b/packages/mermaid/src/diagrams/er/styles.js index 45a9dd7ec..ae4bc38f1 100644 --- a/packages/mermaid/src/diagrams/er/styles.js +++ b/packages/mermaid/src/diagrams/er/styles.js @@ -36,8 +36,8 @@ const getStyles = (options) => .node rect, .node circle, .node ellipse, - .node polygon, - .node path { + .node polygon + { fill: ${options.mainBkg}; stroke: ${options.nodeBorder}; stroke-width: 1px; From 29c3293265a32e4f6c69dcbb5ae69a72c1e41cb8 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 15 Oct 2024 11:42:47 -0700 Subject: [PATCH 009/177] Update documentation --- docs/syntax/entityRelationshipDiagram.md | 378 ++++++++++++++++-- .../docs/syntax/entityRelationshipDiagram.md | 255 ++++++++++-- 2 files changed, 577 insertions(+), 56 deletions(-) diff --git a/docs/syntax/entityRelationshipDiagram.md b/docs/syntax/entityRelationshipDiagram.md index 693cb53c7..175899994 100644 --- a/docs/syntax/entityRelationshipDiagram.md +++ b/docs/syntax/entityRelationshipDiagram.md @@ -92,7 +92,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to Where: -- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v10.5.0+), and may also contain digits and hyphens. +- `first-entity` is the name of an entity. Names support any unicode characters and can include spaces if surrounded by double quotes (e.g. "name with space"). - `relationship` describes the way that both entities inter-relate. See below. - `second-entity` is the name of the other entity. - `relationship-label` describes the relationship from the perspective of the first entity. @@ -107,6 +107,34 @@ This statement can be read as _a property contains one or more rooms, and a room Only the `first-entity` part of a statement is mandatory. This makes it possible to show an entity with no relationships, which can be useful during iterative construction of diagrams. If any other parts of a statement are specified, then all parts are mandatory. +#### Unicode text + +Entity names, relationships, and attributes all support unicode text. + +```mermaid-example +erDiagram + "This ❤ Unicode" +``` + +```mermaid +erDiagram + "This ❤ Unicode" +``` + +#### Markdown formatting + +Markdown formatting and text is also supported. + +```mermaid-example +erDiagram + "This **is** _Markdown_" +``` + +```mermaid +erDiagram + "This **is** _Markdown_" +``` + ### Relationship Syntax The `relationship` part of each statement can be broken down into three sub-components: @@ -145,6 +173,11 @@ Cardinality is a property that describes how many elements of another entity can Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line: +| Value | Alias for | +| :---: | :---------------: | +| -- | _identifying_ | +| .. | _non-identifying_ | + **Aliases** | Value | Alias for | @@ -155,13 +188,25 @@ Relationships may be classified as either _identifying_ or _non-identifying_ and ```mermaid-example erDiagram CAR ||--o{ NAMED-DRIVER : allows - PERSON ||--o{ NAMED-DRIVER : is + PERSON }o..o{ NAMED-DRIVER : is ``` ```mermaid erDiagram CAR ||--o{ NAMED-DRIVER : allows - PERSON ||--o{ NAMED-DRIVER : is + PERSON }o..o{ NAMED-DRIVER : is +``` + +```mermaid-example +erDiagram + CAR 1 to zero or more NAMED-DRIVER : allows + PERSON many(0) optionally to 0+ NAMED-DRIVER : is +``` + +```mermaid +erDiagram + CAR 1 to zero or more NAMED-DRIVER : allows + PERSON many(0) optionally to 0+ NAMED-DRIVER : is ``` ### Attributes @@ -202,9 +247,9 @@ erDiagram The `type` values must begin with an alphabetic character and may contain digits, hyphens, underscores, parentheses and square brackets. The `name` values follow a similar format to `type`, but may start with an asterisk as another option to indicate an attribute is a primary key. Other than that, there are no restrictions, and there is no implicit set of valid data types. -### Entity Name Aliases (v10.5.0+) +### Entity Name Aliases -An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. +An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. Alias names follow all of the same rules as entity names. ```mermaid-example erDiagram @@ -232,7 +277,7 @@ erDiagram #### Attribute Keys and Comments -Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key. To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them. +Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key (markdown formatting and unicode is not supported for keys). To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them. ```mermaid-example erDiagram @@ -282,35 +327,312 @@ erDiagram MANUFACTURER only one to zero or more CAR : makes ``` -### Other Things +### Direction -- If you want the relationship label to be more than one word, you must use double quotes around the phrase -- If you don't want a label at all on a relationship, you must use an empty double-quoted string -- (v11.1.0+) If you want a multi-line label on a relationship, use `
` between the two lines (`"first line
second line"`) +The direction statement declares the direction of the diagram. -## Styling +This declares that the diagram is oriented from top to bottom (`TB`). This can be reversed to be oriented from bottom to top (`BT`). -### Config options +```mermaid-example +erDiagram + direction TB + CUSTOMER ||--o{ ORDER : places + CUSTOMER { + string name + string custNumber + string sector + } + ORDER ||--|{ LINE-ITEM : contains + ORDER { + int orderNumber + string deliveryAddress + } + LINE-ITEM { + string productCode + int quantity + float pricePerUnit + } +``` -For simple color customization: +```mermaid +erDiagram + direction TB + CUSTOMER ||--o{ ORDER : places + CUSTOMER { + string name + string custNumber + string sector + } + ORDER ||--|{ LINE-ITEM : contains + ORDER { + int orderNumber + string deliveryAddress + } + LINE-ITEM { + string productCode + int quantity + float pricePerUnit + } +``` -| Name | Used as | -| :------- | :------------------------------------------------------------------- | -| `fill` | Background color of an entity or attribute | -| `stroke` | Border color of an entity or attribute, line color of a relationship | +This declares that the diagram is oriented from left to right (`LR`). This can be reversed to be oriented from right to left (`RL`). -### Classes used +```mermaid-example +erDiagram + direction LR + CUSTOMER ||--o{ ORDER : places + CUSTOMER { + string name + string custNumber + string sector + } + ORDER ||--|{ LINE-ITEM : contains + ORDER { + int orderNumber + string deliveryAddress + } + LINE-ITEM { + string productCode + int quantity + float pricePerUnit + } +``` -The following CSS class selectors are available for richer styling: +```mermaid +erDiagram + direction LR + CUSTOMER ||--o{ ORDER : places + CUSTOMER { + string name + string custNumber + string sector + } + ORDER ||--|{ LINE-ITEM : contains + ORDER { + int orderNumber + string deliveryAddress + } + LINE-ITEM { + string productCode + int quantity + float pricePerUnit + } +``` -| Selector | Description | -| :------------------------- | :---------------------------------------------------- | -| `.er.attributeBoxEven` | The box containing attributes on even-numbered rows | -| `.er.attributeBoxOdd` | The box containing attributes on odd-numbered rows | -| `.er.entityBox` | The box representing an entity | -| `.er.entityLabel` | The label for an entity | -| `.er.relationshipLabel` | The label for a relationship | -| `.er.relationshipLabelBox` | The box surrounding a relationship label | -| `.er.relationshipLine` | The line representing a relationship between entities | +Possible diagram orientations are: + +- TB - Top to bottom +- BT - Bottom to top +- RL - Right to left +- LR - Left to right + +### Styling a node + +It is possible to apply specific styles such as a thicker border or a different background color to a node. + +```mermaid-example +erDiagram + id1||--||id2 : label + style id1 fill:#f9f,stroke:#333,stroke-width:4px + style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 +``` + +```mermaid +erDiagram + id1||--||id2 : label + style id1 fill:#f9f,stroke:#333,stroke-width:4px + style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 +``` + +It is also possible to attach styles to a list of nodes in one statement: + +``` + style nodeId1,nodeId2 styleList +``` + +#### Classes + +More convenient than defining the style every time is to define a class of styles and attach this class to the nodes that +should have a different look. + +A class definition looks like the example below: + +``` + classDef className fill:#f9f,stroke:#333,stroke-width:4px +``` + +It is also possible to define multiple classes in one statement: + +``` + classDef firstClassName,secondClassName font-size:12pt +``` + +Attachment of a class to a node is done as per below: + +``` + class nodeId1 className +``` + +It is also possible to attach a class to a list of nodes in one statement: + +``` + class nodeId1,nodeId2 className +``` + +Multiple classes can be attached at the same time as well: + +``` + class nodeId1,nodeId2 className1,className2 +``` + +A shorter form of adding a class is to attach the classname to the node using the `:::`operator as per below: + +```mermaid-example +erDiagram + direction TB + CAR:::someclass { + string registrationNumber + string make + string model + } + PERSON:::someclass { + string firstName + string lastName + int age + } + HOUSE:::someclass + + classDef someclass fill:#f96 +``` + +```mermaid +erDiagram + direction TB + CAR:::someclass { + string registrationNumber + string make + string model + } + PERSON:::someclass { + string firstName + string lastName + int age + } + HOUSE:::someclass + + classDef someclass fill:#f96 +``` + +This form can be used when declaring relationships between entities: + +```mermaid-example +erDiagram + CAR { + string registrationNumber + string make + string model + } + PERSON { + string firstName + string lastName + int age + } + PERSON:::foo ||--|| CAR : owns + PERSON o{--|| HOUSE:::bar : has + + classDef foo stroke:#f00 + classDef bar stroke:#0f0 + classDef foobar stroke:#00f +``` + +```mermaid +erDiagram + CAR { + string registrationNumber + string make + string model + } + PERSON { + string firstName + string lastName + int age + } + PERSON:::foo ||--|| CAR : owns + PERSON o{--|| HOUSE:::bar : has + + classDef foo stroke:#f00 + classDef bar stroke:#0f0 + classDef foobar stroke:#00f +``` + +Similar to the class statement, the shorthand syntax can also apply multiple classes at once: + +``` + nodeId:::className1,className2 +``` + +### Default class + +If a class is named default it will be assigned to all classes without specific class definitions. + +```mermaid-example +erDiagram + CAR { + string registrationNumber + string make + string model + } + PERSON { + string firstName + string lastName + int age + } + PERSON:::foo ||--|| CAR : owns + PERSON o{--|| HOUSE:::bar : has + + classDef default fill:#f9f,stroke-width:4px + classDef foo stroke:#f00 + classDef bar stroke:#0f0 + classDef foobar stroke:#00f +``` + +```mermaid +erDiagram + CAR { + string registrationNumber + string make + string model + } + PERSON { + string firstName + string lastName + int age + } + PERSON:::foo ||--|| CAR : owns + PERSON o{--|| HOUSE:::bar : has + + classDef default fill:#f9f,stroke-width:4px + classDef foo stroke:#f00 + classDef bar stroke:#0f0 + classDef foobar stroke:#00f +``` + +## Configuration + +### Renderer + +The layout of the diagram is done with the renderer. The default renderer is dagre. + +You can opt to use an alternate renderer named elk by editing the configuration. The elk renderer is better for larger and/or more complex diagrams. + +``` +--- + config: + layout: elk +--- +``` + +> **Note** +> Note that the site needs to use mermaid version 9.4+ for this to work and have this featured enabled in the lazy-loading configuration. diff --git a/packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md b/packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md index 8c83d2232..6ff3c6abb 100644 --- a/packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md +++ b/packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md @@ -56,7 +56,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to Where: -- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v10.5.0+), and may also contain digits and hyphens. +- `first-entity` is the name of an entity. Names support any unicode characters and can include spaces if surrounded by double quotes (e.g. "name with space"). - `relationship` describes the way that both entities inter-relate. See below. - `second-entity` is the name of the other entity. - `relationship-label` describes the relationship from the perspective of the first entity. @@ -71,6 +71,24 @@ This statement can be read as _a property contains one or more rooms, and a room Only the `first-entity` part of a statement is mandatory. This makes it possible to show an entity with no relationships, which can be useful during iterative construction of diagrams. If any other parts of a statement are specified, then all parts are mandatory. +#### Unicode text + +Entity names, relationships, and attributes all support unicode text. + +```mermaid-example +erDiagram + "This ❤ Unicode" +``` + +#### Markdown formatting + +Markdown formatting and text is also supported. + +```mermaid-example +erDiagram + "This **is** _Markdown_" +``` + ### Relationship Syntax The `relationship` part of each statement can be broken down into three sub-components: @@ -109,6 +127,11 @@ Cardinality is a property that describes how many elements of another entity can Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line: +| Value | Alias for | +| :---: | :---------------: | +| -- | _identifying_ | +| .. | _non-identifying_ | + **Aliases** | Value | Alias for | @@ -116,10 +139,16 @@ Relationships may be classified as either _identifying_ or _non-identifying_ and | to | _identifying_ | | optionally to | _non-identifying_ | -```mermaid +```mermaid-example erDiagram CAR ||--o{ NAMED-DRIVER : allows - PERSON ||--o{ NAMED-DRIVER : is + PERSON }o..o{ NAMED-DRIVER : is +``` + +```mermaid-example +erDiagram + CAR 1 to zero or more NAMED-DRIVER : allows + PERSON many(0) optionally to 0+ NAMED-DRIVER : is ``` ### Attributes @@ -144,9 +173,9 @@ erDiagram The `type` values must begin with an alphabetic character and may contain digits, hyphens, underscores, parentheses and square brackets. The `name` values follow a similar format to `type`, but may start with an asterisk as another option to indicate an attribute is a primary key. Other than that, there are no restrictions, and there is no implicit set of valid data types. -### Entity Name Aliases (v10.5.0+) +### Entity Name Aliases -An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. +An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. Alias names follow all of the same rules as entity names. ```mermaid-example erDiagram @@ -162,7 +191,7 @@ erDiagram #### Attribute Keys and Comments -Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key. To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them. +Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key (markdown formatting and unicode is not supported for keys). To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them. ```mermaid-example erDiagram @@ -188,35 +217,205 @@ erDiagram MANUFACTURER only one to zero or more CAR : makes ``` -### Other Things +### Direction -- If you want the relationship label to be more than one word, you must use double quotes around the phrase -- If you don't want a label at all on a relationship, you must use an empty double-quoted string -- (v11.1.0+) If you want a multi-line label on a relationship, use `
` between the two lines (`"first line
second line"`) +The direction statement declares the direction of the diagram. -## Styling +This declares that the diagram is oriented from top to bottom (`TB`). This can be reversed to be oriented from bottom to top (`BT`). -### Config options +```mermaid-example +erDiagram + direction TB + CUSTOMER ||--o{ ORDER : places + CUSTOMER { + string name + string custNumber + string sector + } + ORDER ||--|{ LINE-ITEM : contains + ORDER { + int orderNumber + string deliveryAddress + } + LINE-ITEM { + string productCode + int quantity + float pricePerUnit + } +``` -For simple color customization: +This declares that the diagram is oriented from left to right (`LR`). This can be reversed to be oriented from right to left (`RL`). -| Name | Used as | -| :------- | :------------------------------------------------------------------- | -| `fill` | Background color of an entity or attribute | -| `stroke` | Border color of an entity or attribute, line color of a relationship | +```mermaid-example +erDiagram + direction LR + CUSTOMER ||--o{ ORDER : places + CUSTOMER { + string name + string custNumber + string sector + } + ORDER ||--|{ LINE-ITEM : contains + ORDER { + int orderNumber + string deliveryAddress + } + LINE-ITEM { + string productCode + int quantity + float pricePerUnit + } +``` -### Classes used +Possible diagram orientations are: -The following CSS class selectors are available for richer styling: +- TB - Top to bottom +- BT - Bottom to top +- RL - Right to left +- LR - Left to right -| Selector | Description | -| :------------------------- | :---------------------------------------------------- | -| `.er.attributeBoxEven` | The box containing attributes on even-numbered rows | -| `.er.attributeBoxOdd` | The box containing attributes on odd-numbered rows | -| `.er.entityBox` | The box representing an entity | -| `.er.entityLabel` | The label for an entity | -| `.er.relationshipLabel` | The label for a relationship | -| `.er.relationshipLabelBox` | The box surrounding a relationship label | -| `.er.relationshipLine` | The line representing a relationship between entities | +### Styling a node + +It is possible to apply specific styles such as a thicker border or a different background color to a node. + +```mermaid-example +erDiagram + id1||--||id2 : label + style id1 fill:#f9f,stroke:#333,stroke-width:4px + style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 +``` + +It is also possible to attach styles to a list of nodes in one statement: + +``` + style nodeId1,nodeId2 styleList +``` + +#### Classes + +More convenient than defining the style every time is to define a class of styles and attach this class to the nodes that +should have a different look. + +A class definition looks like the example below: + +``` + classDef className fill:#f9f,stroke:#333,stroke-width:4px +``` + +It is also possible to define multiple classes in one statement: + +``` + classDef firstClassName,secondClassName font-size:12pt +``` + +Attachment of a class to a node is done as per below: + +``` + class nodeId1 className +``` + +It is also possible to attach a class to a list of nodes in one statement: + +``` + class nodeId1,nodeId2 className +``` + +Multiple classes can be attached at the same time as well: + +``` + class nodeId1,nodeId2 className1,className2 +``` + +A shorter form of adding a class is to attach the classname to the node using the `:::`operator as per below: + +```mermaid-example +erDiagram + direction TB + CAR:::someclass { + string registrationNumber + string make + string model + } + PERSON:::someclass { + string firstName + string lastName + int age + } + HOUSE:::someclass + + classDef someclass fill:#f96 +``` + +This form can be used when declaring relationships between entities: + +```mermaid-example +erDiagram + CAR { + string registrationNumber + string make + string model + } + PERSON { + string firstName + string lastName + int age + } + PERSON:::foo ||--|| CAR : owns + PERSON o{--|| HOUSE:::bar : has + + classDef foo stroke:#f00 + classDef bar stroke:#0f0 + classDef foobar stroke:#00f +``` + +Similar to the class statement, the shorthand syntax can also apply multiple classes at once: + +``` + nodeId:::className1,className2 +``` + +### Default class + +If a class is named default it will be assigned to all classes without specific class definitions. + +```mermaid-example +erDiagram + CAR { + string registrationNumber + string make + string model + } + PERSON { + string firstName + string lastName + int age + } + PERSON:::foo ||--|| CAR : owns + PERSON o{--|| HOUSE:::bar : has + + classDef default fill:#f9f,stroke-width:4px + classDef foo stroke:#f00 + classDef bar stroke:#0f0 + classDef foobar stroke:#00f +``` + +## Configuration + +### Renderer + +The layout of the diagram is done with the renderer. The default renderer is dagre. + +You can opt to use an alternate renderer named elk by editing the configuration. The elk renderer is better for larger and/or more complex diagrams. + +``` +--- + config: + layout: elk +--- +``` + +```note +Note that the site needs to use mermaid version 9.4+ for this to work and have this featured enabled in the lazy-loading configuration. +``` From 0d664b1fd2bce5acf4d7307394617f3c4ef709b5 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 15 Oct 2024 11:44:02 -0700 Subject: [PATCH 010/177] Add tests --- .../rendering/erDiagram-v3.spec.js | 680 ++++++++++++++++++ .../src/diagrams/er/parser/erDiagram.spec.js | 220 +++++- 2 files changed, 880 insertions(+), 20 deletions(-) create mode 100644 cypress/integration/rendering/erDiagram-v3.spec.js diff --git a/cypress/integration/rendering/erDiagram-v3.spec.js b/cypress/integration/rendering/erDiagram-v3.spec.js new file mode 100644 index 000000000..438c032de --- /dev/null +++ b/cypress/integration/rendering/erDiagram-v3.spec.js @@ -0,0 +1,680 @@ +import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; + +describe('Entity Relationship Diagram', () => { + it('should render a simple ER diagram', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + `, + { logLevel: 1 } + ); + }); + + it('should render a simple ER diagram without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render an ER diagram with a recursive relationship', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER ||..o{ CUSTOMER : refers + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + `, + { logLevel: 1 } + ); + }); + + it('should render an ER diagram with multiple relationships between the same two entities', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER ||--|{ ADDRESS : "invoiced at" + CUSTOMER ||--|{ ADDRESS : "receives goods at" + `, + { logLevel: 1 } + ); + }); + + it('should render a cyclical ER diagram', () => { + imgSnapshotTest( + ` + erDiagram + A ||--|{ B : likes + B ||--|{ C : likes + C ||--|{ A : likes + `, + { logLevel: 1 } + ); + }); + + it('should render a not-so-simple ER diagram', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER }|..|{ DELIVERY-ADDRESS : has + CUSTOMER ||--o{ ORDER : places + CUSTOMER ||--o{ INVOICE : "liable for" + DELIVERY-ADDRESS ||--o{ ORDER : receives + INVOICE ||--|{ ORDER : covers + ORDER ||--|{ ORDER-ITEM : includes + PRODUCT-CATEGORY ||--|{ PRODUCT : contains + PRODUCT ||--o{ ORDER-ITEM : "ordered in" + `, + { logLevel: 1 } + ); + }); + + it('should render a not-so-simple ER diagram without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER }|..|{ DELIVERY-ADDRESS : has + CUSTOMER ||--o{ ORDER : places + CUSTOMER ||--o{ INVOICE : "liable for" + DELIVERY-ADDRESS ||--o{ ORDER : receives + INVOICE ||--|{ ORDER : covers + ORDER ||--|{ ORDER-ITEM : includes + PRODUCT-CATEGORY ||--|{ PRODUCT : contains + PRODUCT ||--o{ ORDER-ITEM : "ordered in" + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render multiple ER diagrams', () => { + imgSnapshotTest( + [ + ` + erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + `, + ` + erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE-ITEM : contains + `, + ], + { logLevel: 1 } + ); + }); + + it('should render an ER diagram with blank or empty labels', () => { + imgSnapshotTest( + ` + erDiagram + BOOK }|..|{ AUTHOR : "" + BOOK }|..|{ GENRE : " " + AUTHOR }|..|{ GENRE : " " + `, + { logLevel: 1 } + ); + }); + + it('should render entities that have no relationships', () => { + renderGraph( + ` + erDiagram + DEAD_PARROT + HERMIT + RECLUSE + SOCIALITE }o--o{ SOCIALITE : "interacts with" + RECLUSE }o--o{ SOCIALITE : avoids + `, + { logLevel: 1 } + ); + }); + + it('should render entities with and without attributes', () => { + renderGraph( + ` + erDiagram + BOOK { string title } + AUTHOR }|..|{ BOOK : writes + BOOK { float price } + `, + { logLevel: 1 } + ); + }); + + it('should render entities with generic and array attributes', () => { + renderGraph( + ` + erDiagram + BOOK { + string title + string[] authors + type~T~ type + } + `, + { logLevel: 1 } + ); + }); + + it('should render entities with generic and array attributes without htmlLabels', () => { + renderGraph( + ` + erDiagram + BOOK { + string title + string[] authors + type~T~ type + } + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render entities with length in attributes type', () => { + renderGraph( + ` + erDiagram + CLUSTER { + varchar(99) name + string(255) description + } + `, + { logLevel: 1 } + ); + }); + + it('should render entities with length in attributes type without htmlLabels', () => { + renderGraph( + ` + erDiagram + CLUSTER { + varchar(99) name + string(255) description + } + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render entities and attributes with big and small entity names', () => { + renderGraph( + ` + erDiagram + PRIVATE_FINANCIAL_INSTITUTION { + string name + int turnover + } + PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs + EMPLOYEE { bool officer_of_firm } + `, + { logLevel: 1 } + ); + }); + + it('should render entities and attributes with big and small entity names without htmlLabels', () => { + renderGraph( + ` + erDiagram + PRIVATE_FINANCIAL_INSTITUTION { + string name + int turnover + } + PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs + EMPLOYEE { bool officer_of_firm } + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render entities with attributes that begin with asterisk', () => { + imgSnapshotTest( + ` + erDiagram + BOOK { + int *id + string name + varchar(99) summary + } + BOOK }o..o{ STORE : soldBy + STORE { + int *id + string name + varchar(50) address + } + `, + { loglevel: 1 } + ); + }); + + it('should render entities with attributes that begin with asterisk without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + BOOK { + int *id + string name + varchar(99) summary + } + BOOK }o..o{ STORE : soldBy + STORE { + int *id + string name + varchar(50) address + } + `, + { loglevel: 1, htmlLabels: false } + ); + }); + + it('should render entities with keys', () => { + renderGraph( + ` + erDiagram + AUTHOR_WITH_LONG_ENTITY_NAME { + string name PK + } + AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes + BOOK { + float price + string author FK + string title PK + } + `, + { logLevel: 1 } + ); + }); + + it('should render entities with keys without htmlLabels', () => { + renderGraph( + ` + erDiagram + AUTHOR_WITH_LONG_ENTITY_NAME { + string name PK + } + AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes + BOOK { + float price + string author FK + string title PK + } + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render entities with comments', () => { + renderGraph( + ` + erDiagram + AUTHOR_WITH_LONG_ENTITY_NAME { + string name "comment" + } + AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes + BOOK { + string author + string title "author comment" + float price "price comment" + } + `, + { logLevel: 1 } + ); + }); + + it('should render entities with comments without htmlLabels', () => { + renderGraph( + ` + erDiagram + AUTHOR_WITH_LONG_ENTITY_NAME { + string name "comment" + } + AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes + BOOK { + string author + string title "author comment" + float price "price comment" + } + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render entities with keys and comments', () => { + renderGraph( + ` + erDiagram + AUTHOR_WITH_LONG_ENTITY_NAME { + string name PK "comment" + } + AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes + BOOK { + string description + float price "price comment" + string title PK "title comment" + string author FK + } + `, + { logLevel: 1 } + ); + }); + + it('should render entities with keys and comments without htmlLabels', () => { + renderGraph( + ` + erDiagram + AUTHOR_WITH_LONG_ENTITY_NAME { + string name PK "comment" + } + AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes + BOOK { + string description + float price "price comment" + string title PK "title comment" + string author FK + } + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render entities with aliases', () => { + renderGraph( + ` + erDiagram + T1 one or zero to one or more T2 : test + T2 one or many optionally to zero or one T3 : test + T3 zero or more to zero or many T4 : test + T4 many(0) to many(1) T5 : test + T5 many optionally to one T6 : test + T6 only one optionally to only one T1 : test + T4 0+ to 1+ T6 : test + T1 1 to 1 T3 : test + `, + { logLevel: 1 } + ); + }); + + it('1433: should render a simple ER diagram with a title', () => { + imgSnapshotTest( + `--- +title: simple ER diagram +--- +erDiagram +CUSTOMER ||--o{ ORDER : places +ORDER ||--|{ LINE-ITEM : contains +`, + {} + ); + }); + + it('should render entities with entity name aliases', () => { + imgSnapshotTest( + ` + erDiagram + p[Person] { + varchar(64) firstName + varchar(64) lastName + } + c["Customer Account"] { + varchar(128) email + } + p ||--o| c : has + `, + { logLevel: 1 } + ); + }); + + it('should render relationship labels with line breaks', () => { + imgSnapshotTest( + ` + erDiagram + p[Person] { + string firstName + string lastName + } + a["Customer Account"] { + string email + } + + b["Customer Account Secondary"] { + string email + } + + c["Customer Account Tertiary"] { + string email + } + + d["Customer Account Nth"] { + string email + } + + p ||--o| a : "has
one" + p ||--o| b : "has
one
two" + p ||--o| c : "has
one
two
three" + p ||--o| d : "has
one
two
three
...
Nth" + `, + { logLevel: 1 } + ); + }); + + it('should render an ER diagram with unicode text', () => { + imgSnapshotTest( + ` + erDiagram + _**testẽζ➕Ø😀㌕ぼ**_ { + *__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***" + *string(99)~T~~~~~~* firstName "Only __99__
characters are a
llowed dsfsdfsdfsdfs" + string last*Name* + string __phone__ UK + int _age_ + } + `, + { logLevel: 1 } + ); + }); + + it('should render an ER diagram with unicode text without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + _**testẽζ➕Ø😀㌕ぼ**_ { + *__List~List~int~~sdfds__* **driversLicense** PK "***The l😀icense #***" + *string(99)~T~~~~~~* firstName "Only __99__
characters are a
llowed dsfsdfsdfsdfs" + string last*Name* + string __phone__ UK + int _age_ + } + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render an ER diagram with relationships with unicode text', () => { + imgSnapshotTest( + ` + erDiagram + person[😀] { + string *first*Name + string _**last**Name_ + } + a["*Customer Account*"] { + **string** ema*i*l + } + person ||--o| a : __hẽ😀__ + `, + { logLevel: 1 } + ); + }); + + it('should render an ER diagram with relationships with unicode text without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + person[😀] { + string *first*Name + string _**last**Name_ + } + a["*Customer Account*"] { + **string** ema*i*l + } + person ||--o| a : __hẽ😀__ + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render an ER diagram with TB direction', () => { + imgSnapshotTest( + ` + erDiagram + direction TB + CAR ||--|{ NAMED-DRIVER : allows + PERSON ||..o{ NAMED-DRIVER : is + `, + { logLevel: 1 } + ); + }); + + it('should render an ER diagram with BT direction', () => { + imgSnapshotTest( + ` + erDiagram + direction BT + CAR ||--|{ NAMED-DRIVER : allows + PERSON ||..o{ NAMED-DRIVER : is + `, + { logLevel: 1 } + ); + }); + + it('should render an ER diagram with LR direction', () => { + imgSnapshotTest( + ` + erDiagram + direction LR + CAR ||--|{ NAMED-DRIVER : allows + PERSON ||..o{ NAMED-DRIVER : is + `, + { logLevel: 1 } + ); + }); + + it('should render an ER diagram with RL direction', () => { + imgSnapshotTest( + ` + erDiagram + direction RL + CAR ||--|{ NAMED-DRIVER : allows + PERSON ||..o{ NAMED-DRIVER : is + `, + { logLevel: 1 } + ); + }); + + it('should render entities with styles applied from style statement', () => { + imgSnapshotTest( + ` + erDiagram + c[CUSTOMER] + p[PERSON] + style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold + `, + { logLevel: 1 } + ); + }); + + it('should render entities with styles applied from style statement without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + c[CUSTOMER] + p[PERSON] + style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render entities with styles applied from class statement', () => { + imgSnapshotTest( + ` + erDiagram + c[CUSTOMER] + p[PERSON]:::blue + classDef bold font-size:24px, font-weight: bold + classDef blue stroke:lightblue, color: #0000FF + class c,p bold + `, + { logLevel: 1 } + ); + }); + + it('should render entities with styles applied from class statement without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + c[CUSTOMER] + p[PERSON]:::blue + classDef bold font-size:24px, font-weight: bold + classDef blue stroke:lightblue, color: #0000FF + class c,p bold + `, + { logLevel: 1, htmlLabels: false } + ); + }); + + it('should render entities with the handDrawn look', () => { + imgSnapshotTest( + ` + erDiagram + c[CUSTOMER] + p[PERSON] + `, + { logLevel: 1, look: 'handDrawn' } + ); + }); + + it('should render entities with the handDrawn look without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + c[CUSTOMER] + p[PERSON] + `, + { logLevel: 1, look: 'handDrawn', htmlLabels: false } + ); + }); + + it('should render entities with the handDrawn look and styles applied', () => { + imgSnapshotTest( + ` + erDiagram + c[CUSTOMER] + p[PERSON]:::blue + classDef bold font-size:24px, font-weight: bold + classDef blue stroke:black, fill:lightblue, color: #0000FF + class c,p bold + `, + { logLevel: 1, look: 'handDrawn' } + ); + }); + + it('should render entities with the handDrawn look and styles applied without htmlLabels', () => { + imgSnapshotTest( + ` + erDiagram + c[CUSTOMER] + p[PERSON]:::blue + classDef bold font-size:24px, font-weight: bold + classDef blue stroke:black, fill:lightblue, color: #0000FF + class c,p bold + `, + { logLevel: 1, look: 'handDrawn', htmlLabels: false } + ); + }); +}); diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js index e36454f31..aad485aea 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js @@ -143,32 +143,32 @@ describe('when parsing ER diagram it...', function () { expect(entities.get(entity).alias).toBe(alias); }); - it('can have an alias even if the relationship is defined before class', function () { + it('can have an alias even if the relationship is defined before buzz', function () { const firstEntity = 'foo'; const secondEntity = 'bar'; const alias = 'batman'; erDiagram.parser.parse( - `erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nclass ${firstEntity}["${alias}"]\n` + `erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nbuzz ${firstEntity}["${alias}"]\n` ); const entities = erDb.getEntities(); expect(entities.has(firstEntity)).toBe(true); expect(entities.has(secondEntity)).toBe(true); expect(entities.get(firstEntity).alias).toBe(alias); - expect(entities.get(secondEntity).alias).toBeUndefined(); + expect(entities.get(secondEntity).alias).toBe(''); }); - it('can have an alias even if the relationship is defined after class', function () { + it('can have an alias even if the relationship is defined after buzz', function () { const firstEntity = 'foo'; const secondEntity = 'bar'; const alias = 'batman'; erDiagram.parser.parse( - `erDiagram\nclass ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n` + `erDiagram\nbuzz ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n` ); const entities = erDb.getEntities(); expect(entities.has(firstEntity)).toBe(true); expect(entities.has(secondEntity)).toBe(true); expect(entities.get(firstEntity).alias).toBe(alias); - expect(entities.get(secondEntity).alias).toBeUndefined(); + expect(entities.get(secondEntity).alias).toBe(''); }); it('can start with an underscore', function () { @@ -193,9 +193,9 @@ describe('when parsing ER diagram it...', function () { expect(entities.size).toBe(1); expect(entities.get(entity).attributes.length).toBe(3); - expect(entities.get(entity).attributes[0].attributeName).toBe('myBookTitle'); - expect(entities.get(entity).attributes[1].attributeName).toBe('MYBOOKSUBTITLE_1'); - expect(entities.get(entity).attributes[2].attributeName).toBe('author-ref[name](1)'); + expect(entities.get(entity).attributes[0].name).toBe('myBookTitle'); + expect(entities.get(entity).attributes[1].name).toBe('MYBOOKSUBTITLE_1'); + expect(entities.get(entity).attributes[2].name).toBe('author-ref[name](1)'); }); it('should allow asterisk at the start of attribute name', function () { @@ -258,7 +258,7 @@ describe('when parsing ER diagram it...', function () { const entities = erDb.getEntities(); expect(entities.size).toBe(1); expect(entities.get(entity).attributes.length).toBe(1); - expect(entities.get(entity).attributes[0].attributeComment).toBe('comment'); + expect(entities.get(entity).attributes[0].comment).toBe('comment'); }); it('should allow an entity with a single attribute to be defined with a key and a comment', function () { @@ -297,14 +297,14 @@ describe('when parsing ER diagram it...', function () { `erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n${attribute4}\n${attribute5}\n}` ); const entities = erDb.getEntities(); - expect(entities.get(entity).attributes[0].attributeKeyTypeList).toEqual(['PK', 'FK']); - expect(entities.get(entity).attributes[0].attributeComment).toBe('comment1'); - expect(entities.get(entity).attributes[1].attributeKeyTypeList).toEqual(['PK', 'UK', 'FK']); - expect(entities.get(entity).attributes[2].attributeKeyTypeList).toEqual(['PK', 'UK']); - expect(entities.get(entity).attributes[2].attributeComment).toBe('comment3'); - expect(entities.get(entity).attributes[3].attributeKeyTypeList).toBeUndefined(); - expect(entities.get(entity).attributes[4].attributeKeyTypeList).toBeUndefined(); - expect(entities.get(entity).attributes[4].attributeComment).toBe('comment5'); + expect(entities.get(entity).attributes[0].keys).toEqual(['PK', 'FK']); + expect(entities.get(entity).attributes[0].comment).toBe('comment1'); + expect(entities.get(entity).attributes[1].keys).toEqual(['PK', 'UK', 'FK']); + expect(entities.get(entity).attributes[2].keys).toEqual(['PK', 'UK']); + expect(entities.get(entity).attributes[2].comment).toBe('comment3'); + expect(entities.get(entity).attributes[3].keys).toEqual([]); + expect(entities.get(entity).attributes[4].keys).toEqual([]); + expect(entities.get(entity).attributes[4].comment).toBe('comment5'); }); it('should allow an entity with attribute that has a generic type', function () { @@ -341,8 +341,8 @@ describe('when parsing ER diagram it...', function () { const entities = erDb.getEntities(); expect(entities.size).toBe(1); expect(entities.get(entity).attributes.length).toBe(2); - expect(entities.get(entity).attributes[0].attributeType).toBe('character(10)'); - expect(entities.get(entity).attributes[1].attributeType).toBe('varchar(5)'); + expect(entities.get(entity).attributes[0].type).toBe('character(10)'); + expect(entities.get(entity).attributes[1].type).toBe('varchar(5)'); }); it('should allow an entity with multiple attributes to be defined', function () { @@ -764,6 +764,186 @@ describe('when parsing ER diagram it...', function () { }).toThrowError(); }); + it('should be possible to apply a style to an entity', function () { + const entityName = 'CUSTOMER'; + erDiagram.parser.parse(`erDiagram\n${entityName}\nstyle ${entityName} color:red`); + + expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red']); + }); + + it('should be possible to apply multiple styles to an entity at the same time', function () { + const entityName = 'CUSTOMER'; + erDiagram.parser.parse( + `erDiagram\n${entityName}\nstyle ${entityName} color:red,stroke:blue,fill:#f9f` + ); + + expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red', 'stroke:blue', 'fill:#f9f']); + }); + + it('should be possible to apply multiple separately defined styles', function () { + const entityName = 'CUSTOMER'; + erDiagram.parser.parse( + `erDiagram\n${entityName}\nstyle ${entityName} color:red\nstyle ${entityName} fill:#f9f` + ); + + expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red', 'fill:#f9f']); + }); + + it('should be possible to assign a class to an entity', function () { + const entityName = 'CUSTOMER'; + erDiagram.parser.parse(`erDiagram\n${entityName}\nclass ${entityName} myClass`); + + expect(erDb.getEntity(entityName).cssClasses).toEqual(['default', 'myClass']); + }); + + it('should be possible to assign multiple classes to an entity at the same time', function () { + const entityName = 'CUSTOMER'; + erDiagram.parser.parse( + `erDiagram\n${entityName}\nclass ${entityName} firstClass, secondClass, thirdClass` + ); + + expect(erDb.getEntity(entityName).cssClasses).toEqual([ + 'default', + 'firstClass', + 'secondClass', + 'thirdClass', + ]); + }); + + it('should be possible to assign multiple separately defined classes to an entity', function () { + const entityName = 'CUSTOMER'; + erDiagram.parser.parse( + `erDiagram\n${entityName}\nclass ${entityName} firstClass\nclass ${entityName} secondClass` + ); + + expect(erDb.getEntity(entityName).cssClasses).toEqual(['default', 'firstClass', 'secondClass']); + }); + + it('should be possible to configure the default class and have it apply to each entity', function () { + const firstEntity = 'ENTITY1'; + const secondEntity = 'ENTITY2'; + erDiagram.parser.parse( + `erDiagram\n${firstEntity}\n${secondEntity}\nclassDef default fill:#f9f` + ); + + const expectedOutput = new Map([ + [ + 'default', + { + id: 'default', + styles: ['fill:#f9f'], + textStyles: [], + }, + ], + ]); + + expect(erDb.getEntity(firstEntity).cssClasses).toEqual(['default']); + expect(erDb.getEntity(secondEntity).cssClasses).toEqual(['default']); + expect(erDb.getClasses()).toEqual(expectedOutput); + }); + + it('should be possible to define a class with styles', function () { + const className = 'myClass'; + const styles = 'fill:#f9f, stroke: red, color: pink'; + erDiagram.parser.parse(`erDiagram\nclassDef ${className} ${styles}`); + + const expectedOutput = new Map([ + [ + className, + { + id: className, + styles: ['fill:#f9f', 'stroke:red', 'color:pink'], + textStyles: ['color:pink'], + }, + ], + ]); + + expect(erDb.getClasses()).toEqual(expectedOutput); + }); + + it('should be possible to define multiple class with styles at the same time', function () { + const firstClass = 'firstClass'; + const secondClass = 'secondClass'; + const styles = 'fill:#f9f, stroke: red, color: pink'; + erDiagram.parser.parse(`erDiagram\nclassDef ${firstClass},${secondClass} ${styles}`); + + const expectedOutput = new Map([ + [ + firstClass, + { + id: firstClass, + styles: ['fill:#f9f', 'stroke:red', 'color:pink'], + textStyles: ['color:pink'], + }, + ], + [ + secondClass, + { + id: secondClass, + styles: ['fill:#f9f', 'stroke:red', 'color:pink'], + textStyles: ['color:pink'], + }, + ], + ]); + + expect(erDb.getClasses()).toEqual(expectedOutput); + }); + + it('should be possible to assign a class using the shorthand syntax just by itself', function () { + const entityName = 'CUSTOMER'; + const className = 'myClass'; + erDiagram.parser.parse(`erDiagram\n${entityName}:::${className}`); + + expect(erDb.getEntity(entityName).cssClasses).toEqual(['default', 'myClass']); + }); + + it('should be possible to assign a class using the shorthand syntax with empty block', function () { + const entityName = 'CUSTOMER'; + const className = 'myClass'; + erDiagram.parser.parse(`erDiagram\n${entityName}:::${className} {}`); + + expect(erDb.getEntity(entityName).cssClasses).toEqual(['default', 'myClass']); + }); + + it('should be possible to assign a class using the shorthand syntax with block of attributes', function () { + const entityName = 'CUSTOMER'; + const className = 'myClass'; + erDiagram.parser.parse(`erDiagram\n${entityName}:::${className} {\nstring name\n}`); + + expect(erDb.getEntity(entityName).cssClasses).toEqual(['default', 'myClass']); + }); + + it('should be possible to assign multiple classes using the shorthand syntax', function () { + const entityName = 'CUSTOMER'; + const firstClass = 'firstClass'; + const secondClass = 'secondClass'; + erDiagram.parser.parse(`erDiagram\n${entityName}:::${firstClass},${secondClass}`); + + expect(erDb.getEntity(entityName).cssClasses).toEqual(['default', 'firstClass', 'secondClass']); + }); + + it('should be possible to assign classes using the shorthand syntax after defining an alias', function () { + const entityName = 'c'; + const entityAlias = 'CUSTOMER'; + const myClass = 'myClass'; + erDiagram.parser.parse(`erDiagram\n${entityName}[${entityAlias}]:::${myClass}`); + + expect(erDb.getEntity(entityName).alias).toBe(entityAlias); + expect(erDb.getEntity(entityName).cssClasses).toEqual(['default', 'myClass']); + }); + + it('should be possible to assign classes using the shorthand syntax while defining a relationship', function () { + const entityName = 'CUSTOMER'; + const otherEntity = 'PERSON'; + const myClass = 'myClass'; + erDiagram.parser.parse( + `erDiagram\n${entityName}:::${myClass} ||--o{ ${otherEntity}:::${myClass} : allows` + ); + + expect(erDb.getEntity(entityName).cssClasses).toEqual(['default', 'myClass']); + expect(erDb.getEntity(otherEntity).cssClasses).toEqual(['default', 'myClass']); + }); + describe('relationship labels', function () { it('should allow an empty quoted label', function () { erDiagram.parser.parse('erDiagram\nCUSTOMER ||--|{ ORDER : ""'); From 1d4ea56740b106f99e90bc01aa5ea849b62bfa36 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 15 Oct 2024 11:44:58 -0700 Subject: [PATCH 011/177] Support direction data --- packages/mermaid/src/diagrams/er/erDb.ts | 2 +- packages/mermaid/src/diagrams/er/erRenderer-unified.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/er/erDb.ts b/packages/mermaid/src/diagrams/er/erDb.ts index ef0b5382d..ccf543797 100644 --- a/packages/mermaid/src/diagrams/er/erDb.ts +++ b/packages/mermaid/src/diagrams/er/erDb.ts @@ -110,7 +110,7 @@ const addRelationship = function (entA: string, rolA: string, entB: string, rSpe const getRelationships = () => relationships; -const getDirection = () => direction; +export const getDirection = () => direction; const setDirection = (dir: string) => { direction = dir; }; diff --git a/packages/mermaid/src/diagrams/er/erRenderer-unified.ts b/packages/mermaid/src/diagrams/er/erRenderer-unified.ts index be668df69..4679a6f04 100644 --- a/packages/mermaid/src/diagrams/er/erRenderer-unified.ts +++ b/packages/mermaid/src/diagrams/er/erRenderer-unified.ts @@ -4,7 +4,7 @@ import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import type { LayoutData } from '../../rendering-util/types.js'; -import db from './erDb.js'; +import { getDirection } from './erDb.js'; import utils from '../../utils.js'; export const draw = async function (text: string, id: string, _version: string, diag: any) { @@ -25,7 +25,7 @@ export const draw = async function (text: string, id: string, _version: string, // Workaround as when rendering and setting up the graph it uses flowchart spacing before data4Layout spacing? data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140; data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80; - data4Layout.direction = db.getDirection(); + data4Layout.direction = getDirection(); data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more']; data4Layout.diagramId = id; From b5da91b29a79bd0ceb097480b62e659ed0c406e4 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 15 Oct 2024 11:45:59 -0700 Subject: [PATCH 012/177] Update --- cypress/platform/yari2.html | 163 ++++++++++++++++++++-- pnpm-lock.yaml | 262 +++++++++++++++++++++++++++++------- 2 files changed, 363 insertions(+), 62 deletions(-) diff --git a/cypress/platform/yari2.html b/cypress/platform/yari2.html index 35fa2493b..9c3ce490c 100644 --- a/cypress/platform/yari2.html +++ b/cypress/platform/yari2.html @@ -15,7 +15,7 @@ _**hiØ**_[*test*] { *__List~List~int~~sdfds__* __driversLicense__ PK "***The l😀icense #***" *string(99)~T~~~~~~* firstName "Only 99
characters are a
llowed dsfsdfsdfsdfs" - string lastName + ~str ing~ lastName string phone UK int age } @@ -27,15 +27,23 @@

Basic ErNode

-          ---
-            config:
-              htmlLabels: true
-              look: handDrawn
-          ---
           erDiagram
-            c[CUST😀OMER]
-            p[__PERSON__]
-            style c,p fill:#f9f,stroke:blue, color:grey, font-size:24px,font-weight:bold
+            CAR {
+                string registrationNumber
+                string make
+                string model
+            }
+            PERSON {
+                string firstName
+                string lastName
+                int age
+            }
+
+            CAR:::someclass
+            PERSON:::anotherclass,someclass
+        
+            classDef someclass fill:#f96
+            classDef anotherclass color:blue
         
@@ -59,7 +67,7 @@ --- config: htmlLabels: false - look: + look: --- erDiagram CAR ||--|{ NAMED-DRIVER : allows @@ -121,10 +129,139 @@

Basic Relationship

+---
+  config:
+    layout: elk
+---
           erDiagram
-            CUSTOMER ||--o{ CUSTOMER : refers
-            CUSTOMER ||--o{ ORDER : places
-            ORDER ||--|{ LINE-ITEM : contains
+          rental{
+                  ~timestamp with time zone~ rental_date "NN"
+                  ~integer~ inventory_id "NN"
+                  ~integer~ customer_id "NN"
+                  ~timestamp with time zone~ return_date
+                  ~integer~ staff_id "NN"
+                  ~integer~ rental_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          film_actor{
+                  ~integer~ actor_id "NN"
+                  ~integer~ film_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          film{
+                  ~text~ title "NN"
+                  ~text~ description
+                  ~public.year~ release_year
+                  ~integer~ language_id "NN"
+                  ~integer~ original_language_id
+                  ~smallint~ length
+                  ~text[]~ special_features
+                  ~tsvector~ fulltext "NN"
+                  ~integer~ film_id "NN"
+                  ~smallint~ rental_duration "NN"
+                  ~numeric(4,2)~ rental_rate "NN"
+                  ~numeric(5,2)~ replacement_cost "NN"
+                  ~public.mpaa_rating~ rating
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          customer{
+                  ~integer~ store_id "NN"
+                  ~text~ first_name "NN"
+                  ~text~ last_name "NN"
+                  ~text~ email
+                  ~integer~ address_id "NN"
+                  ~integer~ active
+                  ~integer~ customer_id "NN"
+                  ~boolean~ activebool "NN"
+                  ~date~ create_date "NN"
+                  ~timestamp with time zone~ last_update
+  }
+          film_category{
+                  ~integer~ film_id "NN"
+                  ~integer~ category_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          actor{
+                  ~text~ first_name "NN"
+                  ~text~ last_name "NN"
+                  ~integer~ actor_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          store{
+                  ~integer~ manager_staff_id "NN"
+                  ~integer~ address_id "NN"
+                  ~integer~ store_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          city{
+                  ~text~ city "NN"
+                  ~integer~ country_id "NN"
+                  ~integer~ city_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          language{
+                  ~character(20)~ name "NN"
+                  ~integer~ language_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          payment{
+                  ~integer~ customer_id "NN"
+                  ~integer~ staff_id "NN"
+                  ~integer~ rental_id "NN"
+                  ~numeric(5,2)~ amount "NN"
+                  ~timestamp with time zone~ payment_date "NN"
+                  ~integer~ payment_id "NN"
+  }
+          category{
+                  ~text~ name "NN"
+                  ~integer~ category_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          inventory{
+                  ~integer~ film_id "NN"
+                  ~integer~ store_id "NN"
+                  ~integer~ inventory_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          address{
+                  ~text~ address "NN"
+                  ~text~ address2
+                  ~text~ district "NN"
+                  ~integer~ city_id "NN"
+                  ~text~ postal_code
+                  ~text~ phone "NN"
+                  ~integer~ address_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          staff{
+                  ~text~ first_name "NN"
+                  ~text~ last_name "NN"
+                  ~integer~ address_id "NN"
+                  ~text~ email
+                  ~integer~ store_id "NN"
+                  ~text~ username "NN"
+                  ~text~ password
+                  ~bytea~ picture
+                  ~integer~ staff_id "NN"
+                  ~boolean~ active "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+          country{
+                  ~text~ country "NN"
+                  ~integer~ country_id "NN"
+                  ~timestamp with time zone~ last_update "NN"
+  }
+  film_actor }|..|| film : film_actor_film_id_fkey
+  film_actor }|..|| actor : film_actor_actor_id_fkey
+  address }|..|| city : address_city_id_fkey
+  city }|..|| country : city_country_id_fkey
+  customer }|..|| store : customer_store_id_fkey
+  customer }|..|| address : customer_address_id_fkey
+  film }|..|| language : film_original_language_id_fkey
+  film }|..|| language : film_language_id_fkey
+  film_category }|..|| film : film_category_film_id_fkey
+  film_category }|..|| category : film_category_category_id_fkey
+  inventory }|..|| store : inventory_store_id_fkey
         
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b23b384b6..9045a46db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -443,6 +443,67 @@ importers: version: link:../mermaid packages/mermaid/src/docs: + dependencies: + '@mdi/font': + specifier: ^7.0.0 + version: 7.4.47 + '@vueuse/core': + specifier: ^10.9.0 + version: 10.11.1(vue@3.4.38(typescript@5.4.5)) + font-awesome: + specifier: ^4.7.0 + version: 4.7.0 + jiti: + specifier: ^1.21.0 + version: 1.21.6 + mermaid: + specifier: workspace:^ + version: link:../.. + vue: + specifier: ^3.4.21 + version: 3.4.38(typescript@5.4.5) + devDependencies: + '@iconify-json/carbon': + specifier: ^1.1.31 + version: 1.1.37 + '@unocss/reset': + specifier: ^0.59.0 + version: 0.59.4 + '@vite-pwa/vitepress': + specifier: ^0.4.0 + version: 0.4.0(vite-plugin-pwa@0.19.8(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0)) + '@vitejs/plugin-vue': + specifier: ^5.0.0 + version: 5.1.2(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0))(vue@3.4.38(typescript@5.4.5)) + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 + https-localhost: + specifier: ^4.7.1 + version: 4.7.1 + pathe: + specifier: ^1.1.2 + version: 1.1.2 + unocss: + specifier: ^0.59.0 + version: 0.59.4(postcss@8.4.41)(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0)) + unplugin-vue-components: + specifier: ^0.26.0 + version: 0.26.0(@babel/parser@7.25.6)(rollup@4.21.1)(vue@3.4.38(typescript@5.4.5)) + vite: + specifier: ^5.0.0 + version: 5.4.2(@types/node@22.5.1)(terser@5.33.0) + vite-plugin-pwa: + specifier: ^0.19.7 + version: 0.19.8(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) + vitepress: + specifier: 1.1.4 + version: 1.1.4(@algolia/client-search@4.24.0)(@types/node@22.5.1)(axios@1.7.5)(postcss@8.4.41)(search-insights@2.15.0)(terser@5.33.0)(typescript@5.4.5) + workbox-window: + specifier: ^7.0.0 + version: 7.1.0 + + packages/mermaid/src/vitepress: dependencies: '@mdi/font': specifier: ^7.0.0 @@ -9600,7 +9661,7 @@ snapshots: '@argos-ci/util': 2.1.1 axios: 1.7.5(debug@4.3.7) convict: 6.2.4 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) fast-glob: 3.3.2 sharp: 0.33.5 tmp: 0.2.3 @@ -9641,7 +9702,7 @@ snapshots: '@babel/traverse': 7.25.3 '@babel/types': 7.25.2 convert-source-map: 2.0.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -9719,7 +9780,7 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -10444,7 +10505,7 @@ snapshots: '@babel/parser': 7.25.4 '@babel/template': 7.25.0 '@babel/types': 7.25.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -10456,7 +10517,7 @@ snapshots: '@babel/parser': 7.25.6 '@babel/template': 7.25.0 '@babel/types': 7.25.6 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -11008,7 +11069,7 @@ snapshots: '@babel/preset-env': 7.25.4(@babel/core@7.25.2) babel-loader: 9.1.3(@babel/core@7.25.2)(webpack@5.93.0(esbuild@0.21.5)) bluebird: 3.7.1 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) lodash: 4.17.21 webpack: 5.93.0(esbuild@0.21.5) transitivePeerDependencies: @@ -11209,7 +11270,7 @@ snapshots: '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -11219,7 +11280,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) espree: 10.1.0 globals: 14.0.0 ignore: 5.3.2 @@ -11304,7 +11365,7 @@ snapshots: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.10 '@iconify/types': 2.0.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 0.5.0 mlly: 1.7.1 @@ -11702,14 +11763,6 @@ snapshots: picomatch: 2.3.1 rollup: 2.79.1 - '@rollup/pluginutils@5.1.0(rollup@2.79.1)': - dependencies: - '@types/estree': 1.0.5 - estree-walker: 2.0.2 - picomatch: 2.3.1 - optionalDependencies: - rollup: 2.79.1 - '@rollup/pluginutils@5.1.0(rollup@4.21.1)': dependencies: '@types/estree': 1.0.5 @@ -11726,6 +11779,14 @@ snapshots: optionalDependencies: rollup: 2.79.1 + '@rollup/pluginutils@5.1.2(rollup@4.21.1)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 2.3.1 + optionalDependencies: + rollup: 4.21.1 + '@rollup/rollup-android-arm-eabi@4.21.1': optional: true @@ -12026,7 +12087,7 @@ snapshots: '@types/eslint@9.6.0': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 '@types/estree@0.0.39': {} @@ -12279,7 +12340,7 @@ snapshots: '@typescript-eslint/types': 8.6.0 '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 8.6.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) eslint: 9.11.1(jiti@1.21.6) optionalDependencies: typescript: 5.4.5 @@ -12300,7 +12361,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.4.5) '@typescript-eslint/utils': 8.6.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.4.5) - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) ts-api-utils: 1.3.0(typescript@5.4.5) optionalDependencies: typescript: 5.4.5 @@ -12316,7 +12377,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.5.0 '@typescript-eslint/visitor-keys': 8.5.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -12331,7 +12392,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.6.0 '@typescript-eslint/visitor-keys': 8.6.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -12384,10 +12445,38 @@ snapshots: transitivePeerDependencies: - rollup + '@unocss/astro@0.59.4(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0))': + dependencies: + '@unocss/core': 0.59.4 + '@unocss/reset': 0.59.4 + '@unocss/vite': 0.59.4(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0)) + optionalDependencies: + vite: 5.4.2(@types/node@22.5.1)(terser@5.33.0) + transitivePeerDependencies: + - rollup + '@unocss/cli@0.59.4(rollup@2.79.1)': dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@2.79.1) + '@rollup/pluginutils': 5.1.2(rollup@2.79.1) + '@unocss/config': 0.59.4 + '@unocss/core': 0.59.4 + '@unocss/preset-uno': 0.59.4 + cac: 6.7.14 + chokidar: 3.6.0 + colorette: 2.0.20 + consola: 3.2.3 + fast-glob: 3.3.2 + magic-string: 0.30.11 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + transitivePeerDependencies: + - rollup + + '@unocss/cli@0.59.4(rollup@4.21.1)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.2(rollup@4.21.1) '@unocss/config': 0.59.4 '@unocss/core': 0.59.4 '@unocss/preset-uno': 0.59.4 @@ -12514,7 +12603,23 @@ snapshots: '@unocss/vite@0.59.4(rollup@2.79.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0))': dependencies: '@ampproject/remapping': 2.3.0 - '@rollup/pluginutils': 5.1.0(rollup@2.79.1) + '@rollup/pluginutils': 5.1.2(rollup@2.79.1) + '@unocss/config': 0.59.4 + '@unocss/core': 0.59.4 + '@unocss/inspector': 0.59.4 + '@unocss/scope': 0.59.4 + '@unocss/transformer-directives': 0.59.4 + chokidar: 3.6.0 + fast-glob: 3.3.2 + magic-string: 0.30.11 + vite: 5.4.2(@types/node@22.5.1)(terser@5.33.0) + transitivePeerDependencies: + - rollup + + '@unocss/vite@0.59.4(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.2(rollup@4.21.1) '@unocss/config': 0.59.4 '@unocss/core': 0.59.4 '@unocss/inspector': 0.59.4 @@ -12702,7 +12807,7 @@ snapshots: '@vueuse/shared': 10.11.1(vue@3.4.38(typescript@5.4.5)) vue-demi: 0.14.10(vue@3.4.38(typescript@5.4.5)) optionalDependencies: - axios: 1.7.5(debug@4.3.7) + axios: 1.7.5 focus-trap: 7.5.4 transitivePeerDependencies: - '@vue/composition-api' @@ -12714,7 +12819,7 @@ snapshots: '@vueuse/shared': 11.0.3(vue@3.4.38(typescript@5.4.5)) vue-demi: 0.14.10(vue@3.4.38(typescript@5.4.5)) optionalDependencies: - axios: 1.7.5(debug@4.3.7) + axios: 1.7.5 focus-trap: 7.5.4 transitivePeerDependencies: - '@vue/composition-api' @@ -12930,13 +13035,13 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color agent-base@7.1.1: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -13125,7 +13230,7 @@ snapshots: avvio@7.2.5: dependencies: archy: 1.0.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) fastq: 1.17.1 queue-microtask: 1.2.3 transitivePeerDependencies: @@ -13143,6 +13248,15 @@ snapshots: transitivePeerDependencies: - debug + axios@1.7.5: + dependencies: + follow-redirects: 1.15.6(debug@4.3.6) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + optional: true + axios@1.7.5(debug@4.3.7): dependencies: follow-redirects: 1.15.6(debug@4.3.7) @@ -14268,9 +14382,11 @@ snapshots: optionalDependencies: supports-color: 8.1.1 - debug@4.3.7: + debug@4.3.7(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 decamelize@1.2.0: {} @@ -14655,7 +14771,7 @@ snapshots: '@es-joy/jsdoccomment': 0.48.0 are-docs-informative: 0.0.2 comment-parser: 1.4.1 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint: 9.11.1(jiti@1.21.6) espree: 10.1.0 @@ -14742,7 +14858,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.0.2 eslint-visitor-keys: 4.0.0 @@ -14951,7 +15067,7 @@ snapshots: extract-zip@2.0.1(supports-color@8.1.1): dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -15110,7 +15226,7 @@ snapshots: dependencies: chalk: 4.1.2 commander: 5.1.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -15168,7 +15284,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.7): optionalDependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) font-awesome@4.7.0: {} @@ -15542,14 +15658,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -15568,7 +15684,7 @@ snapshots: http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.6(debug@4.3.7) + follow-redirects: 1.15.6(debug@4.3.6) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -15598,14 +15714,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -15901,7 +16017,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -15910,7 +16026,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -17011,7 +17127,7 @@ snapshots: micromark@2.11.4: dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -17019,7 +17135,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.1 @@ -18304,7 +18420,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -18315,7 +18431,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -18942,14 +19058,62 @@ snapshots: - rollup - supports-color + unocss@0.59.4(postcss@8.4.41)(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0)): + dependencies: + '@unocss/astro': 0.59.4(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0)) + '@unocss/cli': 0.59.4(rollup@4.21.1) + '@unocss/core': 0.59.4 + '@unocss/extractor-arbitrary-variants': 0.59.4 + '@unocss/postcss': 0.59.4(postcss@8.4.41) + '@unocss/preset-attributify': 0.59.4 + '@unocss/preset-icons': 0.59.4 + '@unocss/preset-mini': 0.59.4 + '@unocss/preset-tagify': 0.59.4 + '@unocss/preset-typography': 0.59.4 + '@unocss/preset-uno': 0.59.4 + '@unocss/preset-web-fonts': 0.59.4 + '@unocss/preset-wind': 0.59.4 + '@unocss/reset': 0.59.4 + '@unocss/transformer-attributify-jsx': 0.59.4 + '@unocss/transformer-attributify-jsx-babel': 0.59.4 + '@unocss/transformer-compile-class': 0.59.4 + '@unocss/transformer-directives': 0.59.4 + '@unocss/transformer-variant-group': 0.59.4 + '@unocss/vite': 0.59.4(rollup@4.21.1)(vite@5.4.2(@types/node@22.5.1)(terser@5.33.0)) + optionalDependencies: + vite: 5.4.2(@types/node@22.5.1)(terser@5.33.0) + transitivePeerDependencies: + - postcss + - rollup + - supports-color + unpipe@1.0.0: {} unplugin-vue-components@0.26.0(@babel/parser@7.25.6)(rollup@2.79.1)(vue@3.4.38(typescript@5.4.5)): dependencies: '@antfu/utils': 0.7.10 - '@rollup/pluginutils': 5.1.0(rollup@2.79.1) + '@rollup/pluginutils': 5.1.2(rollup@2.79.1) chokidar: 3.6.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) + fast-glob: 3.3.2 + local-pkg: 0.4.3 + magic-string: 0.30.11 + minimatch: 9.0.5 + resolve: 1.22.8 + unplugin: 1.12.0 + vue: 3.4.38(typescript@5.4.5) + optionalDependencies: + '@babel/parser': 7.25.6 + transitivePeerDependencies: + - rollup + - supports-color + + unplugin-vue-components@0.26.0(@babel/parser@7.25.6)(rollup@4.21.1)(vue@3.4.38(typescript@5.4.5)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.2(rollup@4.21.1) + chokidar: 3.6.0 + debug: 4.3.7(supports-color@8.1.1) fast-glob: 3.3.2 local-pkg: 0.4.3 magic-string: 0.30.11 @@ -19030,7 +19194,7 @@ snapshots: vite-node@1.6.0(@types/node@20.16.2)(terser@5.33.0): dependencies: cac: 6.7.14 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7(supports-color@8.1.1) pathe: 1.1.2 picocolors: 1.0.1 vite: 5.4.2(@types/node@20.16.2)(terser@5.33.0) From 8b169f3796d7ed4fadc62d6e0c4415fb4e4925e2 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 15 Oct 2024 11:46:31 -0700 Subject: [PATCH 013/177] Fix issue with text content using < and > --- .../rendering-elements/shapes/erBox.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts index 2c812b862..937d3a921 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts @@ -280,29 +280,27 @@ async function addText( labelText = labelText.replaceAll('<', '<').replaceAll('>', '>'); } - const text = label - .node() - .appendChild( - await createText( - label, - labelText, - { - width: calculateTextWidth(labelText, config) + 100, - style, - useHtmlLabels: config.htmlLabels, - }, - config - ) - ); + const text = label.node().appendChild( + await createText( + label, + labelText, + { + width: calculateTextWidth(labelText, config) + 100, + style, + useHtmlLabels: config.htmlLabels, + }, + config + ) + ); // Undo work around now that text passed through correctly - if (labelText.includes('<')) { + if (labelText.includes('<') || labelText.includes('>')) { let child = text.children[0]; - // Get last child + child.textContent = child.textContent.replaceAll('<', '<').replaceAll('>', '>'); while (child.childNodes[0]) { child = child.childNodes[0]; + // Replace its text content + child.textContent = child.textContent.replaceAll('<', '<').replaceAll('>', '>'); } - // Replace its text content - child.textContent = child.textContent.replaceAll('<', '<').replaceAll('>', '>'); } let bbox = text.getBBox(); From 49e23c74bce79d704d222a255e5aeff260ee7f60 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 15 Oct 2024 12:19:35 -0700 Subject: [PATCH 014/177] Update parser to include changes --- .../src/diagrams/er/parser/erDiagram.jison | 123 ++++++++++++++++-- 1 file changed, 109 insertions(+), 14 deletions(-) diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison index 135efc784..668595f43 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison @@ -14,6 +14,10 @@ accDescr\s*":"\s* { this.begin("ac accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} [\}] { this.popState(); } [^\}]* return "acc_descr_multiline_value"; +.*direction\s+TB[^\n]* return 'direction_tb'; +.*direction\s+BT[^\n]* return 'direction_bt'; +.*direction\s+RL[^\n]* return 'direction_rl'; +.*direction\s+LR[^\n]* return 'direction_lr'; [\n]+ return 'NEWLINE'; \s+ /* skip whitespace */ [\s]+ return 'SPACE'; @@ -21,11 +25,15 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili \"[^"]*\" return 'WORD'; "erDiagram" return 'ER_DIAGRAM'; "{" { this.begin("block"); return 'BLOCK_START'; } -"," return 'COMMA'; +\# return 'BRKT'; +"#" return 'BRKT'; +"," return 'COMMA'; +":::" return 'STYLE_SEPARATOR'; +":" return 'COLON'; \s+ /* skip whitespace in block */ \b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY' -(.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD'; -[\*A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD' +([^\s]*)[~].*[~]([^\s]*) return 'ATTRIBUTE_WORD'; +([\*A-Za-z_\u00C0-\uFFFF][A-Za-z0-9\-\_\[\]\(\)\u00C0-\uFFFF\*]*) return 'ATTRIBUTE_WORD'; \"[^"]*\" return 'COMMENT'; [\n]+ /* nothing */ "}" { this.popState(); return 'BLOCK_STOP'; } @@ -33,6 +41,9 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili "[" return 'SQS'; "]" return 'SQE'; +"style" return 'STYLE'; +"classDef" return 'CLASSDEF'; +"class" return 'CLASS'; "one or zero" return 'ZERO_OR_ONE'; "one or more" return 'ONE_OR_MORE'; "one or many" return 'ONE_OR_MORE'; @@ -61,7 +72,8 @@ o\{ return 'ZERO_OR_MORE'; "optionally to" return 'NON_IDENTIFYING'; \.\- return 'NON_IDENTIFYING'; \-\. return 'NON_IDENTIFYING'; -[A-Za-z_][A-Za-z0-9\-_]* return 'ALPHANUM'; +([^\x00-\x7F]|\w|\-|\*)+ return 'UNICODE_TEXT'; +[0-9] return 'NUM'; . return yytext[0]; <> return 'EOF'; @@ -88,35 +100,118 @@ line statement - : entityName relSpec entityName ':' role + : entityName relSpec entityName COLON role { yy.addEntity($1); yy.addEntity($3); yy.addRelationship($1, $5, $3, $2); } + | entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList COLON role + { + yy.addEntity($1); + yy.addEntity($5); + yy.addRelationship($1, $9, $5, $4); + yy.setClass([$1], $3); + yy.setClass([$5], $7); + } + | entityName STYLE_SEPARATOR idList relSpec entityName COLON role + { + yy.addEntity($1); + yy.addEntity($5); + yy.addRelationship($1, $7, $5, $4); + yy.setClass([$1], $3); + } + | entityName relSpec entityName STYLE_SEPARATOR idList COLON role + { + yy.addEntity($1); + yy.addEntity($3); + yy.addRelationship($1, $7, $3, $2); + yy.setClass([$3], $5); + } | entityName BLOCK_START attributes BLOCK_STOP { yy.addEntity($1); yy.addAttributes($1, $3); } + | entityName STYLE_SEPARATOR idList BLOCK_START attributes BLOCK_STOP + { + yy.addEntity($1); + yy.addAttributes($1, $5); + yy.setClass([$1], $3); + } | entityName BLOCK_START BLOCK_STOP { yy.addEntity($1); } + | entityName STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1); yy.setClass([$1], $3); } | entityName { yy.addEntity($1); } + | entityName STYLE_SEPARATOR idList { yy.addEntity($1); yy.setClass([$1], $3); } | entityName SQS entityName SQE BLOCK_START attributes BLOCK_STOP { yy.addEntity($1, $3); yy.addAttributes($1, $6); } + | entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START attributes BLOCK_STOP + { + yy.addEntity($1, $3); + yy.addAttributes($1, $8); + yy.setClass([$1], $6); + + } | entityName SQS entityName SQE BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); } + | entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); yy.setClass([$1], $6); } | entityName SQS entityName SQE { yy.addEntity($1, $3); } + | entityName SQS entityName SQE STYLE_SEPARATOR idList { yy.addEntity($1, $3); yy.setClass([$1], $6); } | title title_value { $$=$2.trim();yy.setAccTitle($$); } | 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($$); } + | direction + | classDefStatement + | classStatement + | styleStatement ; +direction + : direction_tb + { yy.setDirection('TB');} + | direction_bt + { yy.setDirection('BT');} + | direction_rl + { yy.setDirection('RL');} + | direction_lr + { yy.setDirection('LR');} + ; + +classDefStatement + : CLASSDEF idList stylesOpt {$$ = $CLASSDEF;yy.addClass($idList,$stylesOpt);} + ; + +idList + : UNICODE_TEXT { $$ = [$UNICODE_TEXT]; } + | idList COMMA UNICODE_TEXT = { $$ = $idList.concat([$UNICODE_TEXT]); } + ; + +classStatement + : CLASS idList idList {$$ = $CLASS;yy.setClass($2, $3);} + ; + +styleStatement + : STYLE idList stylesOpt {$$ = $STYLE;yy.addCssStyles($2,$stylesOpt);} + ; + +stylesOpt + : style { $$ = [$style] } + | stylesOpt COMMA style {$stylesOpt.push($style);$$ = $stylesOpt;} + ; + +style + : styleComponent + | style styleComponent { $$ = $style + $styleComponent; } + ; + +styleComponent: UNICODE_TEXT | NUM | COLON | BRKT | STYLE; + entityName - : 'ALPHANUM' { $$ = $1; } - | 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); } + : 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); } + | 'UNICODE_TEXT' { $$ = $1; } ; attributes @@ -125,10 +220,10 @@ attributes ; attribute - : attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; } - | attributeType attributeName attributeKeyTypeList { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3 }; } - | attributeType attributeName attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; } - | attributeType attributeName attributeKeyTypeList attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3, attributeComment: $4 }; } + : attributeType attributeName { $$ = { type: $1, name: $2 }; } + | attributeType attributeName attributeKeyTypeList { $$ = { type: $1, name: $2, keys: $3 }; } + | attributeType attributeName attributeComment { $$ = { type: $1, name: $2, comment: $3 }; } + | attributeType attributeName attributeKeyTypeList attributeComment { $$ = { type: $1, name: $2, keys: $3, comment: $4 }; } ; @@ -142,7 +237,7 @@ attributeName attributeKeyTypeList : attributeKeyType { $$ = [$1]; } - | attributeKeyTypeList COMMA attributeKeyType { $1.push($3); $$ = $1; } + | attributeKeyTypeList ',' attributeKeyType { $1.push($3); $$ = $1; } ; attributeKeyType @@ -177,7 +272,7 @@ relType role : 'WORD' { $$ = $1.replace(/"/g, ''); } | 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); } - | 'ALPHANUM' { $$ = $1; } + | 'UNICODE_TEXT' { $$ = $1; } ; -%% +%% \ No newline at end of file From 5be5aa3dfad36785320fc9bd9c3a9bc7e99817f2 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 16 Oct 2024 08:14:13 -0700 Subject: [PATCH 015/177] Register erBox shape --- .../src/rendering-util/rendering-elements/shapes.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts index b336fc823..e19b13ec2 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts @@ -55,6 +55,7 @@ import { triangle } from './shapes/triangle.js'; import { waveEdgedRectangle } from './shapes/waveEdgedRectangle.js'; import { waveRectangle } from './shapes/waveRectangle.js'; import { windowPane } from './shapes/windowPane.js'; +import { erBox } from './shapes/erBox.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ShapeHandler = (parent: any, node: Node, options: ShapeRenderOptions) => unknown; @@ -442,6 +443,14 @@ export const shapesDefs: ShapeDefinition[] = [ aliases: ['lined-document'], handler: linedWaveEdgedRect, }, + { + semanticName: 'Entity Relationship Box', + name: 'Entity Relationship Box', + shortName: 'erBox', + description: 'Entity Relationship Box', + aliases: ['er-box'], + handler: erBox, + }, ]; const generateShapeMap = () => { From 723b8f1d3236bbc95c8fadbf49db82c7e6b1d9fd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:20:09 +0000 Subject: [PATCH 016/177] [autofix.ci] apply automated fixes --- docs/syntax/flowchart.md | 95 ++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/docs/syntax/flowchart.md b/docs/syntax/flowchart.md index 3837e77de..e25851d5f 100644 --- a/docs/syntax/flowchart.md +++ b/docs/syntax/flowchart.md @@ -316,53 +316,54 @@ This syntax creates a node A as a rectangle. It renders in the same way as `A["A Below is a comprehensive list of the newly introduced shapes and their corresponding semantic meanings, short names, and aliases: -| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | -| --------------------------------- | ---------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- | -| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` | -| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` | -| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` | -| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` | -| Comment Right | Curly Brace | `brace-r` | Adds a comment | | -| Comment with braces on both sides | Curly Braces | `braces` | Adds a comment | | -| Data Input/Output | Lean Right | `lean-r` | Represents input or output | `in-out`, `lean-right` | -| Data Input/Output | Lean Left | `lean-l` | Represents output or input | `lean-left`, `out-in` | -| Database | Cylinder | `cyl` | Database storage | `cylinder`, `database`, `db` | -| Decision | Diamond | `diam` | Decision-making step | `decision`, `diamond`, `question` | -| Delay | Half-Rounded Rectangle | `delay` | Represents a delay | `half-rounded-rectangle` | -| Direct Access Storage | Horizontal Cylinder | `h-cyl` | Direct access storage | `das`, `horizontal-cylinder` | -| Disk Storage | Lined Cylinder | `lin-cyl` | Disk storage | `disk`, `lined-cylinder` | -| Display | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` | -| Divided Process | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-process`, `divided-rectangle` | -| Document | Document | `doc` | Represents a document | `doc`, `document` | -| Event | Rounded Rectangle | `rounded` | Represents an event | `event` | -| Extract | Triangle | `tri` | Extraction process | `extract`, `triangle` | -| Fork/Join | Filled Rectangle | `fork` | Fork or join in process flow | `join` | -| Internal Storage | Window Pane | `win-pane` | Internal storage | `internal-storage`, `window-pane` | -| Junction | Filled Circle | `f-circ` | Junction point | `filled-circle`, `junction` | -| Lined Document | Lined Document | `lin-doc` | Lined document | `lined-document` | -| Lined/Shaded Process | Lined Rectangle | `lin-rect` | Lined process shape | `lin-proc`, `lined-process`, `lined-rectangle`, `shaded-process` | -| Loop Limit | Trapezoidal Pentagon | `notch-pent` | Loop limit step | `loop-limit`, `notched-pentagon` | -| Manual File | Flipped Triangle | `flip-tri` | Manual file operation | `flipped-triangle`, `manual-file` | -| Manual Input | Sloped Rectangle | `sl-rect` | Manual input step | `manual-input`, `sloped-rectangle` | -| Manual Operation | Trapezoid Base Top | `trap-t` | Represents a manual task | `inv-trapezoid`, `manual`, `trapezoid-top` | -| Multi-Document | Stacked Document | `docs` | Multiple documents | `documents`, `st-doc`, `stacked-document` | -| Multi-Process | Stacked Rectangle | `st-rect` | Multiple processes | `processes`, `procs`, `stacked-rectangle` | -| Odd | Odd | `odd` | Odd shape | | -| Paper Tape | Flag | `flag` | Paper tape | `paper-tape` | -| Prepare Conditional | Hexagon | `hex` | Preparation or condition step | `hexagon`, `prepare` | -| Priority Action | Trapezoid Base Bottom | `trap-b` | Priority action | `priority`, `trapezoid`, `trapezoid-bottom` | -| Process | Rectangle | `rect` | Standard process shape | `proc`, `process`, `rectangle` | -| Start | Circle | `circle` | Starting point | `circ` | -| Start | Small Circle | `sm-circ` | Small starting point | `small-circle`, `start` | -| Stop | Double Circle | `dbl-circ` | Represents a stop point | `double-circle` | -| Stop | Framed Circle | `fr-circ` | Stop point | `framed-circle`, `stop` | -| Stored Data | Bow Tie Rectangle | `bow-rect` | Stored data | `bow-tie-rectangle`, `stored-data` | -| Subprocess | Framed Rectangle | `fr-rect` | Subprocess | `framed-rectangle`, `subproc`, `subprocess`, `subroutine` | -| Summary | Crossed Circle | `cross-circ` | Summary | `crossed-circle`, `summary` | -| Tagged Document | Tagged Document | `tag-doc` | Tagged document | `tag-doc`, `tagged-document` | -| Tagged Process | Tagged Rectangle | `tag-rect` | Tagged process | `tag-proc`, `tagged-process`, `tagged-rectangle` | -| Terminal Point | Stadium | `stadium` | Terminal point | `pill`, `terminal` | -| Text Block | Text Block | `text` | Text block | | +| **Semantic Name** | **Shape Name** | **Short Name** | **Description** | **Alias Supported** | +| --------------------------------- | ----------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------- | +| Card | Notched Rectangle | `notch-rect` | Represents a card | `card`, `notched-rectangle` | +| Collate | Hourglass | `hourglass` | Represents a collate operation | `collate`, `hourglass` | +| Com Link | Lightning Bolt | `bolt` | Communication link | `com-link`, `lightning-bolt` | +| Comment | Curly Brace | `brace` | Adds a comment | `brace-l`, `comment` | +| Comment Right | Curly Brace | `brace-r` | Adds a comment | | +| Comment with braces on both sides | Curly Braces | `braces` | Adds a comment | | +| Data Input/Output | Lean Right | `lean-r` | Represents input or output | `in-out`, `lean-right` | +| Data Input/Output | Lean Left | `lean-l` | Represents output or input | `lean-left`, `out-in` | +| Database | Cylinder | `cyl` | Database storage | `cylinder`, `database`, `db` | +| Decision | Diamond | `diam` | Decision-making step | `decision`, `diamond`, `question` | +| Delay | Half-Rounded Rectangle | `delay` | Represents a delay | `half-rounded-rectangle` | +| Direct Access Storage | Horizontal Cylinder | `h-cyl` | Direct access storage | `das`, `horizontal-cylinder` | +| Disk Storage | Lined Cylinder | `lin-cyl` | Disk storage | `disk`, `lined-cylinder` | +| Display | Curved Trapezoid | `curv-trap` | Represents a display | `curved-trapezoid`, `display` | +| Divided Process | Divided Rectangle | `div-rect` | Divided process shape | `div-proc`, `divided-process`, `divided-rectangle` | +| Document | Document | `doc` | Represents a document | `doc`, `document` | +| Entity Relationship Box | Entity Relationship Box | `erBox` | Entity Relationship Box | `er-box` | +| Event | Rounded Rectangle | `rounded` | Represents an event | `event` | +| Extract | Triangle | `tri` | Extraction process | `extract`, `triangle` | +| Fork/Join | Filled Rectangle | `fork` | Fork or join in process flow | `join` | +| Internal Storage | Window Pane | `win-pane` | Internal storage | `internal-storage`, `window-pane` | +| Junction | Filled Circle | `f-circ` | Junction point | `filled-circle`, `junction` | +| Lined Document | Lined Document | `lin-doc` | Lined document | `lined-document` | +| Lined/Shaded Process | Lined Rectangle | `lin-rect` | Lined process shape | `lin-proc`, `lined-process`, `lined-rectangle`, `shaded-process` | +| Loop Limit | Trapezoidal Pentagon | `notch-pent` | Loop limit step | `loop-limit`, `notched-pentagon` | +| Manual File | Flipped Triangle | `flip-tri` | Manual file operation | `flipped-triangle`, `manual-file` | +| Manual Input | Sloped Rectangle | `sl-rect` | Manual input step | `manual-input`, `sloped-rectangle` | +| Manual Operation | Trapezoid Base Top | `trap-t` | Represents a manual task | `inv-trapezoid`, `manual`, `trapezoid-top` | +| Multi-Document | Stacked Document | `docs` | Multiple documents | `documents`, `st-doc`, `stacked-document` | +| Multi-Process | Stacked Rectangle | `st-rect` | Multiple processes | `processes`, `procs`, `stacked-rectangle` | +| Odd | Odd | `odd` | Odd shape | | +| Paper Tape | Flag | `flag` | Paper tape | `paper-tape` | +| Prepare Conditional | Hexagon | `hex` | Preparation or condition step | `hexagon`, `prepare` | +| Priority Action | Trapezoid Base Bottom | `trap-b` | Priority action | `priority`, `trapezoid`, `trapezoid-bottom` | +| Process | Rectangle | `rect` | Standard process shape | `proc`, `process`, `rectangle` | +| Start | Circle | `circle` | Starting point | `circ` | +| Start | Small Circle | `sm-circ` | Small starting point | `small-circle`, `start` | +| Stop | Double Circle | `dbl-circ` | Represents a stop point | `double-circle` | +| Stop | Framed Circle | `fr-circ` | Stop point | `framed-circle`, `stop` | +| Stored Data | Bow Tie Rectangle | `bow-rect` | Stored data | `bow-tie-rectangle`, `stored-data` | +| Subprocess | Framed Rectangle | `fr-rect` | Subprocess | `framed-rectangle`, `subproc`, `subprocess`, `subroutine` | +| Summary | Crossed Circle | `cross-circ` | Summary | `crossed-circle`, `summary` | +| Tagged Document | Tagged Document | `tag-doc` | Tagged document | `tag-doc`, `tagged-document` | +| Tagged Process | Tagged Rectangle | `tag-rect` | Tagged process | `tag-proc`, `tagged-process`, `tagged-rectangle` | +| Terminal Point | Stadium | `stadium` | Terminal point | `pill`, `terminal` | +| Text Block | Text Block | `text` | Text block | | ### Example Flowchart with New Shapes From 5d2b0f74af28697d0bc88a40a1497b369550910c Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 16 Oct 2024 09:48:15 -0700 Subject: [PATCH 017/177] Add fix for er markers when using elk layout --- packages/mermaid/src/diagrams/er/erRenderer-unified.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/mermaid/src/diagrams/er/erRenderer-unified.ts b/packages/mermaid/src/diagrams/er/erRenderer-unified.ts index 4679a6f04..084f622ec 100644 --- a/packages/mermaid/src/diagrams/er/erRenderer-unified.ts +++ b/packages/mermaid/src/diagrams/er/erRenderer-unified.ts @@ -30,6 +30,13 @@ export const draw = async function (text: string, id: string, _version: string, data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more']; data4Layout.diagramId = id; await render(data4Layout, svg); + // Elk layout algorithm displays markers above nodes, so cut off the half that covers the node for the larger markers. + if (data4Layout.layoutAlgorithm === 'elk') { + svg.selectAll('*[id*="oneOrMoreStart"]').attr('viewBox', '18 0 38.5 36'); + svg.selectAll('*[id*="oneOrMoreEnd"]').attr('viewBox', '0 0 27 36'); + svg.selectAll('*[id*="zeroOrMoreStart"]').attr('viewBox', '18 0 38.5 36'); + svg.selectAll('*[id*="zeroOrMoreEnd"]').attr('viewBox', '0 0 38.5 36'); + } const padding = 8; utils.insertTitle( svg, From 1d68c4f0755a2d91a881acf8fc899e0aa83dcc3e Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Thu, 17 Oct 2024 17:58:19 -0700 Subject: [PATCH 018/177] Update to test elk layout --- cypress/platform/yari2.html | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/cypress/platform/yari2.html b/cypress/platform/yari2.html index 9c3ce490c..fd38424fd 100644 --- a/cypress/platform/yari2.html +++ b/cypress/platform/yari2.html @@ -55,7 +55,7 @@ --- config: htmlLabels: false - look: + layout: elk --- erDiagram "hi" }o..o{ ORDER : places @@ -129,6 +129,24 @@

Basic Relationship

+          ---
+            config:
+              layout: elk
+          ---
+          erDiagram
+          CUSTOMER }|..|{ DELIVERY-ADDRESS : has
+          CUSTOMER ||--o{ ORDER : places
+          CUSTOMER ||--o{ INVOICE : "liable for"
+          DELIVERY-ADDRESS ||--o{ ORDER : receives
+          INVOICE ||--|{ ORDER : covers
+          ORDER ||--|{ ORDER-ITEM : includes
+          PRODUCT-CATEGORY ||--|{ PRODUCT : contains
+          PRODUCT ||--o{ ORDER-ITEM : "ordered in"
+        
+
+
+

Basic Relationship

+
 ---
   config:
     layout: elk
@@ -267,7 +285,9 @@
     
@@ -112,6 +121,7 @@ onMounted(() => {

Date: Tue, 25 Feb 2025 21:57:38 -0800 Subject: [PATCH 078/177] update styling --- packages/mermaid/src/docs/.vitepress/components/TopBar.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/docs/.vitepress/components/TopBar.vue b/packages/mermaid/src/docs/.vitepress/components/TopBar.vue index ee29e07a8..0914d808e 100644 --- a/packages/mermaid/src/docs/.vitepress/components/TopBar.vue +++ b/packages/mermaid/src/docs/.vitepress/components/TopBar.vue @@ -118,7 +118,7 @@ onMounted(() => { :class="[design === 1 ? 'bg-gradient-to-r from-[#bd34fe] to-[#ff3670] ' : 'bg-[#E0095F]']" class="mb-4 w-full top-bar flex p-2" > -

+

{ class="unstyled flex justify-center items-center gap-4 text-white tracking-wide plausible-event-name=bannerClick" > {{ taglines[index].label }} - From a2650adec2561fa12863f7b90a6ea9b6f5e08c5d Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Wed, 26 Feb 2025 18:07:10 +0530 Subject: [PATCH 079/177] chore: Remove ! in datafetcher Co-authored-by: Saurabh Gore <167211619+saurabhg772244@users.noreply.github.com> --- packages/mermaid/src/diagrams/state/dataFetcher.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/dataFetcher.ts b/packages/mermaid/src/diagrams/state/dataFetcher.ts index 6a84996f8..f38084899 100644 --- a/packages/mermaid/src/diagrams/state/dataFetcher.ts +++ b/packages/mermaid/src/diagrams/state/dataFetcher.ts @@ -199,6 +199,7 @@ export const dataFetcher = ( const dbState = diagramStates.get(itemId); const classStr = getClassesFromDbInfo(dbState); const style = getStylesFromDbInfo(dbState); + const config = getConfig(); log.info('dataFetcher parsedItem', parsedItem, dbState, style); @@ -219,7 +220,7 @@ export const dataFetcher = ( nodeDb.set(itemId, { id: itemId, shape, - description: common.sanitizeText(itemId, getConfig()), + description: common.sanitizeText(itemId, config), cssClasses: `${classStr} ${CSS_DIAGRAM_STATE}`, cssStyles: style, }); @@ -251,7 +252,7 @@ export const dataFetcher = ( newNode.description = parsedItem.description; } } - newNode.description = common.sanitizeTextOrArray(newNode.description, getConfig()); + newNode.description = common.sanitizeTextOrArray(newNode.description, config); } // If there's only 1 description entry, just use a regular state shape @@ -318,7 +319,7 @@ export const dataFetcher = ( domId: stateDomId(itemId, graphItemCount, NOTE), type: newNode.type, isGroup: newNode.type === 'group', - padding: getConfig().flowchart!.padding, + padding: config.flowchart?.padding, look, position: parsedItem.note.position, }; From 840a197044bc4160ca80311b697f2227578d4f44 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 26 Feb 2025 08:45:12 -0800 Subject: [PATCH 080/177] Fix syntax error --- .../mermaid/src/rendering-util/rendering-elements/markers.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/markers.js b/packages/mermaid/src/rendering-util/rendering-elements/markers.js index f9ed00155..fd1c9df20 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/markers.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/markers.js @@ -398,6 +398,8 @@ const zero_or_more = (elem, type, id) => { .attr('orient', 'auto'); endMarker.append('circle').attr('fill', 'white').attr('cx', 9).attr('cy', 18).attr('r', 6); endMarker.append('path').attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18'); +}; + const requirement_arrow = (elem, type, id) => { elem .append('defs') From 40494b16f5ff1ed758c9d84d34efa89a458f2012 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 26 Feb 2025 09:10:53 -0800 Subject: [PATCH 081/177] Update labelBkg styles and dasharray for edges --- packages/mermaid/src/diagrams/er/styles.ts | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 packages/mermaid/src/diagrams/er/styles.ts diff --git a/packages/mermaid/src/diagrams/er/styles.ts b/packages/mermaid/src/diagrams/er/styles.ts new file mode 100644 index 000000000..ecf8cc571 --- /dev/null +++ b/packages/mermaid/src/diagrams/er/styles.ts @@ -0,0 +1,73 @@ +import * as khroma from 'khroma'; +import type { FlowChartStyleOptions } from '../flowchart/styles.js'; + +const fade = (color: string, opacity: number) => { + // @ts-ignore TODO: incorrect types from khroma + const channel = khroma.channel; + + const r = channel(color, 'r'); + const g = channel(color, 'g'); + const b = channel(color, 'b'); + + // @ts-ignore incorrect types from khroma + return khroma.rgba(r, g, b, opacity); +}; + +const getStyles = (options: FlowChartStyleOptions) => + ` + .entityBox { + fill: ${options.mainBkg}; + stroke: ${options.nodeBorder}; + } + + .relationshipLabelBox { + fill: ${options.tertiaryColor}; + opacity: 0.7; + background-color: ${options.tertiaryColor}; + rect { + opacity: 0.5; + } + } + + .labelBkg { + background-color: ${fade(options.edgeLabelBackground, 0.5)}; + } + + .edgeLabel .label { + fill: ${options.nodeBorder}; + font-size: 14px; + } + + .label { + font-family: ${options.fontFamily}; + color: ${options.nodeTextColor || options.textColor}; + } + + .edge-pattern-dashed { + stroke-dasharray: 8,8; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon + { + fill: ${options.mainBkg}; + stroke: ${options.nodeBorder}; + stroke-width: 1px; + } + + .relationshipLine { + stroke: ${options.lineColor}; + stroke-width: 1; + fill: none; + } + + .marker { + fill: none !important; + stroke: ${options.lineColor} !important; + stroke-width: 1; + } +`; + +export default getStyles; From 41281974683ab06031a6928eb01f317717ac9372 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Wed, 26 Feb 2025 09:19:30 -0800 Subject: [PATCH 082/177] Remove to update to .ts --- packages/mermaid/src/diagrams/er/styles.js | 76 ---------------------- 1 file changed, 76 deletions(-) delete mode 100644 packages/mermaid/src/diagrams/er/styles.js diff --git a/packages/mermaid/src/diagrams/er/styles.js b/packages/mermaid/src/diagrams/er/styles.js deleted file mode 100644 index ae4bc38f1..000000000 --- a/packages/mermaid/src/diagrams/er/styles.js +++ /dev/null @@ -1,76 +0,0 @@ -const getStyles = (options) => - ` - .entityBox { - fill: ${options.mainBkg}; - stroke: ${options.nodeBorder}; - } - - .attributeBoxOdd { - fill: ${options.attributeBackgroundColorOdd}; - stroke: ${options.nodeBorder}; - } - - .attributeBoxEven { - fill: ${options.attributeBackgroundColorEven}; - stroke: ${options.nodeBorder}; - } - - .relationshipLabelBox { - fill: ${options.tertiaryColor}; - opacity: 0.7; - background-color: ${options.tertiaryColor}; - rect { - opacity: 0.5; - } - } - - .edgeLabel .label { - fill: ${options.nodeBorder}; - font-size: 14px; - } - - .edgeLabel .label .labelBkg { - background: ${options.mainBkg}; - } - - .node rect, - .node circle, - .node ellipse, - .node polygon - { - fill: ${options.mainBkg}; - stroke: ${options.nodeBorder}; - stroke-width: 1px; - } - - .relationshipLine { - stroke: ${options.lineColor}; - stroke-width: 1; - fill: none; - } - - .marker { - fill: none !important; - stroke: ${options.lineColor} !important; - stroke-width: 1; - } - - .entityTitleText { - text-anchor: middle; - font-size: 18px; - fill: ${options.textColor}; - } - #MD_PARENT_START { - fill: #f5f5f5 !important; - stroke: ${options.lineColor} !important; - stroke-width: 1; - } - #MD_PARENT_END { - fill: #f5f5f5 !important; - stroke: ${options.lineColor} !important; - stroke-width: 1; - } - -`; - -export default getStyles; From 5caaf36a3b02ca1bbc41cf86da5cc7e4e6198bf2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:24:28 +0000 Subject: [PATCH 083/177] [autofix.ci] apply automated fixes --- .../mermaid/src/rendering-util/rendering-elements/shapes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts index 856942e2b..829f89a8f 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes.ts @@ -481,7 +481,7 @@ const generateShapeMap = () => { // er diagram erBox, - + // Requirement diagram requirementBox, } as const; From d0986ace8ceb96db7d0701100d459bd9e0780394 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Fri, 28 Feb 2025 11:05:16 -0800 Subject: [PATCH 084/177] Fix styling of erBox rows --- packages/mermaid/src/diagrams/er/styles.ts | 2 +- .../rendering-elements/shapes/erBox.ts | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/mermaid/src/diagrams/er/styles.ts b/packages/mermaid/src/diagrams/er/styles.ts index ecf8cc571..8ce468550 100644 --- a/packages/mermaid/src/diagrams/er/styles.ts +++ b/packages/mermaid/src/diagrams/er/styles.ts @@ -30,7 +30,7 @@ const getStyles = (options: FlowChartStyleOptions) => } .labelBkg { - background-color: ${fade(options.edgeLabelBackground, 0.5)}; + background-color: ${fade(options.tertiaryColor, 0.5)}; } .edgeLabel .label { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts index 0815ab6c5..26aae412b 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts @@ -142,7 +142,6 @@ export async function erBox(parent: D3Selection TEXT_PADDING; yOffsets.push(yOffset); } - yOffsets.pop(); let totalWidthSections = 4; @@ -228,6 +227,24 @@ export async function erBox(parent: D3Selection const roughRect = rc.rectangle(x, y, w, h, options); const rect = shapeSvg.insert(() => roughRect, ':first-child').attr('style', cssStyles!.join('')); + const { themeVariables } = getConfig(); + const { secondaryColor, tertiaryColor, nodeBorder } = themeVariables; + + yOffsets.push(0); + // Draw row rects + for (const [i, yOffset] of yOffsets.entries()) { + const isEven = i % 2 === 0 && yOffset !== 0; + const roughRect = rc.rectangle(x, nameBBox.height + y + yOffset, w, nameBBox.height, { + ...options, + fill: isEven ? tertiaryColor : secondaryColor, + stroke: nodeBorder, + }); + shapeSvg + .insert(() => roughRect, 'g.label') + .attr('style', cssStyles!.join('')) + .attr('class', `row-rect-${i % 2 === 0 ? 'even' : 'odd'}`); + } + // Draw divider lines // Name line let roughLine = rc.line(x, nameBBox.height + y, w + x, nameBBox.height + y, options); From bb895d8cf437a1da628ec0e9aa0668572afc2fb3 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 12 Mar 2025 10:42:22 +0100 Subject: [PATCH 085/177] #6369 feat: Edge marker rendering applying stroke color to arrow head --- .../rendering-elements/edgeMarker.ts | 77 ++++++++++++++----- .../rendering-elements/edges.js | 13 ++-- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts index 0e65ef18d..a0e9966ba 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts @@ -15,28 +15,29 @@ export const addEdgeMarkers = ( edge: Pick, url: string, id: string, - diagramType: string + diagramType: string, + strokeColor?: string ) => { if (edge.arrowTypeStart) { - addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType); + addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType, strokeColor); } if (edge.arrowTypeEnd) { - addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType); + addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType, strokeColor); } }; const arrowTypesMap = { - arrow_cross: 'cross', - arrow_point: 'point', - arrow_barb: 'barb', - arrow_circle: 'circle', - aggregation: 'aggregation', - extension: 'extension', - composition: 'composition', - dependency: 'dependency', - lollipop: 'lollipop', - requirement_arrow: 'requirement_arrow', - requirement_contains: 'requirement_contains', + arrow_cross: { type: 'cross', fill: false }, + arrow_point: { type: 'point', fill: true }, + arrow_barb: { type: 'barb', fill: true }, + arrow_circle: { type: 'circle', fill: false }, + aggregation: { type: 'aggregation', fill: false }, + extension: { type: 'extension', fill: false }, + composition: { type: 'composition', fill: true }, + dependency: { type: 'dependency', fill: true }, + lollipop: { type: 'lollipop', fill: false }, + requirement_arrow: { type: 'requirement_arrow', fill: false }, + requirement_contains: { type: 'requirement_contains', fill: false }, } as const; const addEdgeMarker = ( @@ -45,15 +46,55 @@ const addEdgeMarker = ( arrowType: string, url: string, id: string, - diagramType: string + diagramType: string, + strokeColor?: string ) => { - const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap]; + const arrowTypeInfo = arrowTypesMap[arrowType as keyof typeof arrowTypesMap]; - if (!endMarkerType) { + if (!arrowTypeInfo) { log.warn(`Unknown arrow type: ${arrowType}`); return; // unknown arrow type, ignore } + const endMarkerType = arrowTypeInfo.type; const suffix = position === 'start' ? 'Start' : 'End'; - svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`); + const originalMarkerId = `${id}_${diagramType}-${endMarkerType}${suffix}`; + + // If stroke color is specified and non-empty, create or use a colored variant of the marker + if (strokeColor && strokeColor.trim() !== '') { + // Create a sanitized color value for use in IDs + const colorId = strokeColor.replace(/[^\dA-Za-z]/g, '_'); + const coloredMarkerId = `${originalMarkerId}_${colorId}`; + + // Check if the colored marker already exists + if (!document.getElementById(coloredMarkerId)) { + // Get the original marker + const originalMarker = document.getElementById(originalMarkerId); + if (originalMarker) { + // Clone the marker and create colored version + const coloredMarker = originalMarker.cloneNode(true) as Element; + coloredMarker.id = coloredMarkerId; + + // Apply colors to the paths inside the marker + const paths = coloredMarker.querySelectorAll('path, circle, line'); + paths.forEach((path) => { + path.setAttribute('stroke', strokeColor); + + // Apply fill only to markers that should be filled + if (arrowTypeInfo.fill) { + path.setAttribute('fill', strokeColor); + } + }); + + // Add the new colored marker to the defs section + originalMarker.parentNode?.appendChild(coloredMarker); + } + } + + // Use the colored marker + svgPath.attr(`marker-${position}`, `url(${url}#${coloredMarkerId})`); + } else { + // Always use the original marker for unstyled edges + svgPath.attr(`marker-${position}`, `url(${url}#${originalMarkerId})`); + } }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 649686c0c..77a8d376f 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -509,6 +509,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod let svgPath; let linePath = lineFunction(lineData); const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style]; + let strokeColor = edgeStyles.find((style) => style.startsWith('stroke:')); if (edge.look === 'handDrawn') { const rc = rough.svg(elem); @@ -539,18 +540,18 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod if (edge.animation) { animationClass = ' edge-animation-' + edge.animation; } + + const pathStyle = stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles; svgPath = elem .append('path') .attr('d', linePath) .attr('id', edge.id) .attr( 'class', - ' ' + - strokeClasses + - (edge.classes ? ' ' + edge.classes : '') + - (animationClass ? animationClass : '') + ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '') ) - .attr('style', stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles); + .attr('style', pathStyle); + strokeColor = pathStyle.match(/stroke:([^;]+)/)?.[1]; } // DEBUG code, DO NOT REMOVE @@ -587,7 +588,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod log.info('arrowTypeStart', edge.arrowTypeStart); log.info('arrowTypeEnd', edge.arrowTypeEnd); - addEdgeMarkers(svgPath, edge, url, id, diagramType); + addEdgeMarkers(svgPath, edge, url, id, diagramType, strokeColor); let paths = {}; if (pointsHasChanged) { From f6335571ccaeef97799def9a7d1e5a1e22a90c13 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 12 Mar 2025 10:45:26 +0100 Subject: [PATCH 086/177] test: Add test for edge marker color rendering --- .../integration/rendering/flowchart.spec.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cypress/integration/rendering/flowchart.spec.js b/cypress/integration/rendering/flowchart.spec.js index d3a83ae5f..7b986cd2f 100644 --- a/cypress/integration/rendering/flowchart.spec.js +++ b/cypress/integration/rendering/flowchart.spec.js @@ -895,7 +895,7 @@ graph TD imgSnapshotTest( ` graph TD - classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff + classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff hello --> default `, { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } @@ -917,4 +917,21 @@ graph TD } ); }); + it('#6369: edge color should affect arrow head', () => { + imgSnapshotTest( + ` + flowchart LR + A --> B + A --> C + C --> D + + linkStyle 0 stroke:#D50000 + linkStyle 2 stroke:#D50000 + `, + { + flowchart: { htmlLabels: true }, + securityLevel: 'loose', + } + ); + }); }); From 4d25caba8e65df078966a283e7e0ae1200bef595 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Wed, 12 Mar 2025 10:52:17 +0100 Subject: [PATCH 087/177] Added changeset --- .changeset/vast-nails-stay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/vast-nails-stay.md diff --git a/.changeset/vast-nails-stay.md b/.changeset/vast-nails-stay.md new file mode 100644 index 000000000..de2059b32 --- /dev/null +++ b/.changeset/vast-nails-stay.md @@ -0,0 +1,5 @@ +--- +'mermaid': minor +--- + +The arrowhead color should match the color of the edge. Creates a unique clone of the arrow marker with the appropriate color. From 1341e3d1562afcc5625144c3e12faf08243d100d Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Wed, 12 Mar 2025 13:13:13 +0100 Subject: [PATCH 088/177] Added support for row odd/even in different themes --- packages/mermaid/src/diagrams/er/erDb.ts | 1 + .../rendering-util/rendering-elements/edges.js | 18 +++++++++++++++--- .../rendering-elements/shapes/erBox.ts | 4 ++-- packages/mermaid/src/themes/theme-base.js | 10 ++++++++++ packages/mermaid/src/themes/theme-dark.js | 4 ++++ packages/mermaid/src/themes/theme-default.js | 14 ++++++++++++++ packages/mermaid/src/themes/theme-forest.js | 4 ++++ packages/mermaid/src/themes/theme-neutral.js | 4 ++++ 8 files changed, 54 insertions(+), 5 deletions(-) diff --git a/packages/mermaid/src/diagrams/er/erDb.ts b/packages/mermaid/src/diagrams/er/erDb.ts index 5e9ba20b6..95f2210b7 100644 --- a/packages/mermaid/src/diagrams/er/erDb.ts +++ b/packages/mermaid/src/diagrams/er/erDb.ts @@ -224,6 +224,7 @@ export class ErDB implements DiagramDB { counter: count++, }), type: 'normal', + curve: 'basis', start: relationship.entityA, end: relationship.entityB, label: relationship.roleA, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 649686c0c..1bee271e0 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -5,7 +5,8 @@ import { createText } from '../createText.js'; import utils from '../../utils.js'; import { getLineFunctionsWithOffset } from '../../utils/lineWithOffset.js'; import { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js'; -import { curveBasis, line, select } from 'd3'; + +import { curveBasis, curveLinear, curveCardinal, line, select } from 'd3'; import rough from 'roughjs'; import createLabel from './createLabel.js'; import { addEdgeMarkers } from './edgeMarker.ts'; @@ -472,8 +473,19 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod let lineData = points.filter((p) => !Number.isNaN(p.y)); lineData = fixCorners(lineData); let curve = curveBasis; - if (edge.curve) { - curve = edge.curve; + curve = curveLinear; + switch (edge.curve) { + case 'linear': + curve = curveLinear; + break; + case 'basis': + curve = curveBasis; + break; + case 'cardinal': + curve = curveCardinal; + break; + default: + curve = curveBasis; } const { x, y } = getLineFunctionsWithOffset(edge); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts index 26aae412b..567197a5a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts @@ -228,7 +228,7 @@ export async function erBox(parent: D3Selection const rect = shapeSvg.insert(() => roughRect, ':first-child').attr('style', cssStyles!.join('')); const { themeVariables } = getConfig(); - const { secondaryColor, tertiaryColor, nodeBorder } = themeVariables; + const { rowEven, rowOdd, nodeBorder } = themeVariables; yOffsets.push(0); // Draw row rects @@ -236,7 +236,7 @@ export async function erBox(parent: D3Selection const isEven = i % 2 === 0 && yOffset !== 0; const roughRect = rc.rectangle(x, nameBBox.height + y + yOffset, w, nameBBox.height, { ...options, - fill: isEven ? tertiaryColor : secondaryColor, + fill: isEven ? rowEven : rowOdd, stroke: nodeBorder, }); shapeSvg diff --git a/packages/mermaid/src/themes/theme-base.js b/packages/mermaid/src/themes/theme-base.js index 6e572ea5f..1f858275f 100644 --- a/packages/mermaid/src/themes/theme-base.js +++ b/packages/mermaid/src/themes/theme-base.js @@ -110,6 +110,16 @@ class Theme { this.personBorder = this.personBorder || this.primaryBorderColor; this.personBkg = this.personBkg || this.mainBkg; + /* ER diagram */ + + if (this.darkMode) { + this.rowOdd = this.rowOdd || darken(this.mainBkg, 5) || '#ffffff'; + this.rowEven = this.rowEven || darken(this.mainBkg, 10); + } else { + this.rowOdd = this.rowOdd || lighten(this.mainBkg, 75) || '#ffffff'; + this.rowEven = this.rowEven || lighten(this.mainBkg, 5); + } + /* state colors */ this.transitionColor = this.transitionColor || this.lineColor; this.transitionLabelColor = this.transitionLabelColor || this.textColor; diff --git a/packages/mermaid/src/themes/theme-dark.js b/packages/mermaid/src/themes/theme-dark.js index cf223d989..a0df8d4f3 100644 --- a/packages/mermaid/src/themes/theme-dark.js +++ b/packages/mermaid/src/themes/theme-dark.js @@ -91,6 +91,10 @@ class Theme { this.archGroupBorderColor = this.primaryBorderColor; this.archGroupBorderWidth = '2px'; + /* Entity Relationship variables */ + this.rowOdd = this.rowOdd || lighten(this.mainBkg, 5) || '#ffffff'; + this.rowEven = this.rowEven || darken(this.mainBkg, 10); + /* state colors */ this.labelColor = 'calculated'; diff --git a/packages/mermaid/src/themes/theme-default.js b/packages/mermaid/src/themes/theme-default.js index e0023758e..78f20a475 100644 --- a/packages/mermaid/src/themes/theme-default.js +++ b/packages/mermaid/src/themes/theme-default.js @@ -119,6 +119,10 @@ class Theme { this.archGroupBorderColor = this.primaryBorderColor; this.archGroupBorderWidth = '2px'; + /* Entity Relationship variables */ + this.rowOdd = 'calculated'; + this.rowEven = 'calculated'; + /* state colors */ this.labelColor = 'black'; this.errorBkgColor = '#552222'; @@ -205,6 +209,9 @@ class Theme { this.archEdgeColor = this.lineColor; this.archEdgeArrowColor = this.lineColor; + /* Entity Relationship variables */ + this.rowOdd = this.rowOdd || lighten(this.primaryColor, 75) || '#ffffff'; + this.rowEven = this.rowEven || lighten(this.primaryColor, 1); /* state colors */ this.transitionColor = this.transitionColor || this.lineColor; this.transitionLabelColor = this.transitionLabelColor || this.textColor; @@ -373,6 +380,13 @@ class Theme { /* -------------------------------------------------- */ } calculate(overrides) { + // for all keys in this object, if it is 'calculated' then set it to undefined + Object.keys(this).forEach((k) => { + if (this[k] === 'calculated') { + this[k] = undefined; + } + }); + if (typeof overrides !== 'object') { // Calculate colors form base colors this.updateColors(); diff --git a/packages/mermaid/src/themes/theme-forest.js b/packages/mermaid/src/themes/theme-forest.js index 97c0682f3..34d965201 100644 --- a/packages/mermaid/src/themes/theme-forest.js +++ b/packages/mermaid/src/themes/theme-forest.js @@ -173,6 +173,10 @@ class Theme { this.archEdgeColor = this.lineColor; this.archEdgeArrowColor = this.lineColor; + /* ER diagram */ + this.rowOdd = this.rowOdd || lighten(this.mainBkg, 75) || '#ffffff'; + this.rowEven = this.rowEven || lighten(this.mainBkg, 20); + /* state colors */ this.transitionColor = this.transitionColor || this.lineColor; this.transitionLabelColor = this.transitionLabelColor || this.textColor; diff --git a/packages/mermaid/src/themes/theme-neutral.js b/packages/mermaid/src/themes/theme-neutral.js index 4a622cbcc..8b7f34ed8 100644 --- a/packages/mermaid/src/themes/theme-neutral.js +++ b/packages/mermaid/src/themes/theme-neutral.js @@ -105,6 +105,10 @@ class Theme { this.archGroupBorderColor = this.primaryBorderColor; this.archGroupBorderWidth = '2px'; + /* ER diagram */ + this.rowOdd = this.rowOdd || lighten(this.mainBkg, 75) || '#ffffff'; + this.rowEven = this.rowEven || '#f4f4f4'; + /* state colors */ this.labelColor = 'black'; From 292cd8343950a6a3de625a72e84f4a8aa65514c9 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Thu, 13 Mar 2025 00:28:03 +0100 Subject: [PATCH 089/177] Fix for multi-line row issue and failsafe for edge style for stroke not present --- .../mermaid/src/rendering-util/rendering-elements/edges.js | 2 +- .../src/rendering-util/rendering-elements/shapes/erBox.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 07d9ab096..4a9ab52cb 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -521,7 +521,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod let svgPath; let linePath = lineFunction(lineData); const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style]; - let strokeColor = edgeStyles.find((style) => style.startsWith('stroke:')); + let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:')); if (edge.look === 'handDrawn') { const rc = rough.svg(elem); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts index 567197a5a..af1e9945a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts @@ -233,6 +233,10 @@ export async function erBox(parent: D3Selection yOffsets.push(0); // Draw row rects for (const [i, yOffset] of yOffsets.entries()) { + if (i === 0 && yOffsets.length > 1) { + continue; + // Skip first row + } const isEven = i % 2 === 0 && yOffset !== 0; const roughRect = rc.rectangle(x, nameBBox.height + y + yOffset, w, nameBBox.height, { ...options, From 05bdf0e20e2629fe77513218fbd4e28e65f75882 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Thu, 13 Mar 2025 11:16:54 +0100 Subject: [PATCH 090/177] Added changeset --- .changeset/poor-masks-change.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/poor-masks-change.md diff --git a/.changeset/poor-masks-change.md b/.changeset/poor-masks-change.md new file mode 100644 index 000000000..2896e7b90 --- /dev/null +++ b/.changeset/poor-masks-change.md @@ -0,0 +1,8 @@ +--- +'mermaid': minor +--- + +Upgrade Requirement and ER diagram to use the common renderer flow + +- Added support for directions +- Added support for hand drawn look From 0c2e1bc3feaf17621aa8aef26c94ec0ebd547574 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 13 Mar 2025 10:24:33 +0000 Subject: [PATCH 091/177] Version Packages --- .changeset/angry-bags-brake.md | 5 --- .changeset/bright-ads-exist.md | 5 --- .changeset/chatty-elephants-warn.md | 5 --- .changeset/chilly-years-cheat.md | 5 --- .changeset/dull-tips-cough.md | 5 --- .changeset/great-ghosts-rule.md | 8 ----- .changeset/grumpy-cheetahs-deliver.md | 5 --- .changeset/heavy-moose-mix.md | 5 --- .changeset/many-brooms-promise.md | 5 --- .changeset/new-kiwis-listen.md | 5 --- .changeset/poor-masks-change.md | 8 ----- .changeset/silver-olives-marry.md | 5 --- .changeset/stupid-dots-do.md | 5 --- .changeset/vast-nails-stay.md | 5 --- .changeset/weak-trees-perform.md | 5 --- .changeset/witty-crews-smell.md | 5 --- packages/mermaid/CHANGELOG.md | 44 +++++++++++++++++++++++++++ packages/mermaid/package.json | 2 +- 18 files changed, 45 insertions(+), 87 deletions(-) delete mode 100644 .changeset/angry-bags-brake.md delete mode 100644 .changeset/bright-ads-exist.md delete mode 100644 .changeset/chatty-elephants-warn.md delete mode 100644 .changeset/chilly-years-cheat.md delete mode 100644 .changeset/dull-tips-cough.md delete mode 100644 .changeset/great-ghosts-rule.md delete mode 100644 .changeset/grumpy-cheetahs-deliver.md delete mode 100644 .changeset/heavy-moose-mix.md delete mode 100644 .changeset/many-brooms-promise.md delete mode 100644 .changeset/new-kiwis-listen.md delete mode 100644 .changeset/poor-masks-change.md delete mode 100644 .changeset/silver-olives-marry.md delete mode 100644 .changeset/stupid-dots-do.md delete mode 100644 .changeset/vast-nails-stay.md delete mode 100644 .changeset/weak-trees-perform.md delete mode 100644 .changeset/witty-crews-smell.md diff --git a/.changeset/angry-bags-brake.md b/.changeset/angry-bags-brake.md deleted file mode 100644 index 472e486ec..000000000 --- a/.changeset/angry-bags-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments diff --git a/.changeset/bright-ads-exist.md b/.changeset/bright-ads-exist.md deleted file mode 100644 index ef2f76f4c..000000000 --- a/.changeset/bright-ads-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -Fixes for consistent edge id creation & handling edge cases for animate edge feature diff --git a/.changeset/chatty-elephants-warn.md b/.changeset/chatty-elephants-warn.md deleted file mode 100644 index 225047ece..000000000 --- a/.changeset/chatty-elephants-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -Fix for issue #6195 - allowing @ signs inside node labels diff --git a/.changeset/chilly-years-cheat.md b/.changeset/chilly-years-cheat.md deleted file mode 100644 index e665af75b..000000000 --- a/.changeset/chilly-years-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram diff --git a/.changeset/dull-tips-cough.md b/.changeset/dull-tips-cough.md deleted file mode 100644 index 1f5179417..000000000 --- a/.changeset/dull-tips-cough.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: revert state db to resolve getData returning empty nodes and edges diff --git a/.changeset/great-ghosts-rule.md b/.changeset/great-ghosts-rule.md deleted file mode 100644 index f11c6e2a9..000000000 --- a/.changeset/great-ghosts-rule.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'mermaid': minor ---- - -Flowchart new syntax for node metadata bugs - -- Incorrect label mapping for nodes when using `&` -- Syntax error when `}` with trailing spaces before new line diff --git a/.changeset/grumpy-cheetahs-deliver.md b/.changeset/grumpy-cheetahs-deliver.md deleted file mode 100644 index fa6736d42..000000000 --- a/.changeset/grumpy-cheetahs-deliver.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -`mermaidAPI.getDiagramFromText()` now returns a new db instance on each call for state diagrams diff --git a/.changeset/heavy-moose-mix.md b/.changeset/heavy-moose-mix.md deleted file mode 100644 index c02d62446..000000000 --- a/.changeset/heavy-moose-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -Added versioning to StateDB and updated tests and diagrams to use it. diff --git a/.changeset/many-brooms-promise.md b/.changeset/many-brooms-promise.md deleted file mode 100644 index fec442b34..000000000 --- a/.changeset/many-brooms-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': minor ---- - -Adding support for animation of flowchart edges diff --git a/.changeset/new-kiwis-listen.md b/.changeset/new-kiwis-listen.md deleted file mode 100644 index 24306573c..000000000 --- a/.changeset/new-kiwis-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each flowchart diff --git a/.changeset/poor-masks-change.md b/.changeset/poor-masks-change.md deleted file mode 100644 index 2896e7b90..000000000 --- a/.changeset/poor-masks-change.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'mermaid': minor ---- - -Upgrade Requirement and ER diagram to use the common renderer flow - -- Added support for directions -- Added support for hand drawn look diff --git a/.changeset/silver-olives-marry.md b/.changeset/silver-olives-marry.md deleted file mode 100644 index d709b17ba..000000000 --- a/.changeset/silver-olives-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages. diff --git a/.changeset/stupid-dots-do.md b/.changeset/stupid-dots-do.md deleted file mode 100644 index 594fa9536..000000000 --- a/.changeset/stupid-dots-do.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: Gantt, Sankey and User Journey diagram are now able to pick font-family from mermaid config. diff --git a/.changeset/vast-nails-stay.md b/.changeset/vast-nails-stay.md deleted file mode 100644 index de2059b32..000000000 --- a/.changeset/vast-nails-stay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': minor ---- - -The arrowhead color should match the color of the edge. Creates a unique clone of the arrow marker with the appropriate color. diff --git a/.changeset/weak-trees-perform.md b/.changeset/weak-trees-perform.md deleted file mode 100644 index 17175301d..000000000 --- a/.changeset/weak-trees-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: `getDirection` and `setDirection` in `stateDb` refactored to return and set actual direction diff --git a/.changeset/witty-crews-smell.md b/.changeset/witty-crews-smell.md deleted file mode 100644 index 4213083f2..000000000 --- a/.changeset/witty-crews-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -`mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram diff --git a/packages/mermaid/CHANGELOG.md b/packages/mermaid/CHANGELOG.md index 221d7c735..49407cda5 100644 --- a/packages/mermaid/CHANGELOG.md +++ b/packages/mermaid/CHANGELOG.md @@ -1,5 +1,49 @@ # mermaid +## 11.5.0 + +### Minor Changes + +- [#6187](https://github.com/mermaid-js/mermaid/pull/6187) [`7809b5a`](https://github.com/mermaid-js/mermaid/commit/7809b5a93fae127f45727071f5ff14325222c518) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Flowchart new syntax for node metadata bugs + + - Incorrect label mapping for nodes when using `&` + - Syntax error when `}` with trailing spaces before new line + +- [#6136](https://github.com/mermaid-js/mermaid/pull/6136) [`ec0d9c3`](https://github.com/mermaid-js/mermaid/commit/ec0d9c389aa6018043187654044c1e0b5aa4f600) Thanks [@knsv](https://github.com/knsv)! - Adding support for animation of flowchart edges + +- [#6373](https://github.com/mermaid-js/mermaid/pull/6373) [`05bdf0e`](https://github.com/mermaid-js/mermaid/commit/05bdf0e20e2629fe77513218fbd4e28e65f75882) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - Upgrade Requirement and ER diagram to use the common renderer flow + + - Added support for directions + - Added support for hand drawn look + +- [#6371](https://github.com/mermaid-js/mermaid/pull/6371) [`4d25cab`](https://github.com/mermaid-js/mermaid/commit/4d25caba8e65df078966a283e7e0ae1200bef595) Thanks [@knsv](https://github.com/knsv)! - The arrowhead color should match the color of the edge. Creates a unique clone of the arrow marker with the appropriate color. + +### Patch Changes + +- [#6064](https://github.com/mermaid-js/mermaid/pull/6064) [`2a91849`](https://github.com/mermaid-js/mermaid/commit/2a91849a38641e97ed6b20cb60aa4506d1b63177) Thanks [@NicolasNewman](https://github.com/NicolasNewman)! - fix: architecture diagrams no longer grow to extreme heights due to conflicting alignments + +- [#6198](https://github.com/mermaid-js/mermaid/pull/6198) [`963efa6`](https://github.com/mermaid-js/mermaid/commit/963efa64c794466dcd0f06bad6de6ba554d05a54) Thanks [@ferozmht](https://github.com/ferozmht)! - Fixes for consistent edge id creation & handling edge cases for animate edge feature + +- [#6196](https://github.com/mermaid-js/mermaid/pull/6196) [`127bac1`](https://github.com/mermaid-js/mermaid/commit/127bac1147034d8a8588cc8f7870abe92ebc945e) Thanks [@knsv](https://github.com/knsv)! - Fix for issue #6195 - allowing @ signs inside node labels + +- [#6212](https://github.com/mermaid-js/mermaid/pull/6212) [`90bbf90`](https://github.com/mermaid-js/mermaid/commit/90bbf90a83bf5da53fc8030cf1370bc8238fa4aa) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each class diagram + +- [#6218](https://github.com/mermaid-js/mermaid/pull/6218) [`232e60c`](https://github.com/mermaid-js/mermaid/commit/232e60c8cbaea804e6d98aa90f90d1ce76730e17) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: revert state db to resolve getData returning empty nodes and edges + +- [#6250](https://github.com/mermaid-js/mermaid/pull/6250) [`9cad3c7`](https://github.com/mermaid-js/mermaid/commit/9cad3c7aea3bbbc61495b23225ccff76d312783f) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - `mermaidAPI.getDiagramFromText()` now returns a new db instance on each call for state diagrams + +- [#6293](https://github.com/mermaid-js/mermaid/pull/6293) [`cfd84e5`](https://github.com/mermaid-js/mermaid/commit/cfd84e54d502f4d36a35b50478121558cfbef2c4) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - Added versioning to StateDB and updated tests and diagrams to use it. + +- [#6161](https://github.com/mermaid-js/mermaid/pull/6161) [`6cc31b7`](https://github.com/mermaid-js/mermaid/commit/6cc31b74530baa6d0f527346ab1395b0896bb3c2) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each flowchart + +- [#6272](https://github.com/mermaid-js/mermaid/pull/6272) [`ffa7804`](https://github.com/mermaid-js/mermaid/commit/ffa7804af0701b3d044d6794e36bd9132d6c7e8d) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: `mermaidAPI.getDiagramFromText()` now returns a new different db for each sequence diagram. Added unique IDs for messages. + +- [#6205](https://github.com/mermaid-js/mermaid/pull/6205) [`32a68d4`](https://github.com/mermaid-js/mermaid/commit/32a68d489ed83a5b79f516d6b2fb3a7505c5eb24) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - fix: Gantt, Sankey and User Journey diagram are now able to pick font-family from mermaid config. + +- [#6295](https://github.com/mermaid-js/mermaid/pull/6295) [`da6361f`](https://github.com/mermaid-js/mermaid/commit/da6361f6527918b4b6a9c07cc9558cf2e2c709d2) Thanks [@omkarht](https://github.com/omkarht)! - fix: `getDirection` and `setDirection` in `stateDb` refactored to return and set actual direction + +- [#6185](https://github.com/mermaid-js/mermaid/pull/6185) [`3e32332`](https://github.com/mermaid-js/mermaid/commit/3e32332814c659e7ed1bb73d4a26ed4e61b77d59) Thanks [@saurabhg772244](https://github.com/saurabhg772244)! - `mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram + ## 11.4.1 ### Patch Changes diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 04911df64..0751d5a5f 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "11.4.1", + "version": "11.5.0", "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.", "type": "module", "module": "./dist/mermaid.core.mjs", From d80cc38bb24a62bb2f2392931963a7db1f3fcd9e Mon Sep 17 00:00:00 2001 From: Thomas Di Cizerone Date: Sun, 16 Mar 2025 18:00:50 +0100 Subject: [PATCH 092/177] =?UTF-8?q?=F0=9F=96=8B=EF=B8=8F=20Add=20grammar?= =?UTF-8?q?=20for=20Radar=20chart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .build/jsonSchema.ts | 1 + packages/parser/langium-config.json | 5 + packages/parser/src/language/index.ts | 3 + packages/parser/src/language/radar/index.ts | 1 + packages/parser/src/language/radar/module.ts | 73 ++++ .../parser/src/language/radar/radar.langium | 89 +++++ .../parser/src/language/radar/tokenBuilder.ts | 7 + packages/parser/src/parse.ts | 10 +- packages/parser/tests/packet.test.ts | 19 + packages/parser/tests/radar.test.ts | 343 ++++++++++++++++++ packages/parser/tests/test-util.ts | 28 ++ 11 files changed, 577 insertions(+), 2 deletions(-) create mode 100644 packages/parser/src/language/radar/index.ts create mode 100644 packages/parser/src/language/radar/module.ts create mode 100644 packages/parser/src/language/radar/radar.langium create mode 100644 packages/parser/src/language/radar/tokenBuilder.ts create mode 100644 packages/parser/tests/packet.test.ts create mode 100644 packages/parser/tests/radar.test.ts diff --git a/.build/jsonSchema.ts b/.build/jsonSchema.ts index 7a700c1e2..48a9883de 100644 --- a/.build/jsonSchema.ts +++ b/.build/jsonSchema.ts @@ -27,6 +27,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [ 'block', 'packet', 'architecture', + 'radar', ] as const; /** diff --git a/packages/parser/langium-config.json b/packages/parser/langium-config.json index bf64493ad..ad80350c2 100644 --- a/packages/parser/langium-config.json +++ b/packages/parser/langium-config.json @@ -25,6 +25,11 @@ "id": "gitGraph", "grammar": "src/language/gitGraph/gitGraph.langium", "fileExtensions": [".mmd", ".mermaid"] + }, + { + "id": "radar", + "grammar": "src/language/radar/radar.langium", + "fileExtensions": [".mmd", ".mermaid"] } ], "mode": "production", diff --git a/packages/parser/src/language/index.ts b/packages/parser/src/language/index.ts index c85a5a8b6..e3aa5c68c 100644 --- a/packages/parser/src/language/index.ts +++ b/packages/parser/src/language/index.ts @@ -7,6 +7,7 @@ export { PieSection, Architecture, GitGraph, + Radar, Branch, Commit, Merge, @@ -31,6 +32,7 @@ export { PieGeneratedModule, ArchitectureGeneratedModule, GitGraphGeneratedModule, + RadarGeneratedModule, } from './generated/module.js'; export * from './gitGraph/index.js'; @@ -39,3 +41,4 @@ export * from './info/index.js'; export * from './packet/index.js'; export * from './pie/index.js'; export * from './architecture/index.js'; +export * from './radar/index.js'; diff --git a/packages/parser/src/language/radar/index.ts b/packages/parser/src/language/radar/index.ts new file mode 100644 index 000000000..fd3c604b0 --- /dev/null +++ b/packages/parser/src/language/radar/index.ts @@ -0,0 +1 @@ +export * from './module.js'; diff --git a/packages/parser/src/language/radar/module.ts b/packages/parser/src/language/radar/module.ts new file mode 100644 index 000000000..de604724a --- /dev/null +++ b/packages/parser/src/language/radar/module.ts @@ -0,0 +1,73 @@ +import type { + DefaultSharedCoreModuleContext, + LangiumCoreServices, + LangiumSharedCoreServices, + Module, + PartialLangiumCoreServices, +} from 'langium'; +import { + EmptyFileSystem, + createDefaultCoreModule, + createDefaultSharedCoreModule, + inject, +} from 'langium'; +import { CommonValueConverter } from '../common/valueConverter.js'; +import { MermaidGeneratedSharedModule, RadarGeneratedModule } from '../generated/module.js'; +import { RadarTokenBuilder } from './tokenBuilder.js'; + +/** + * Declaration of `Radar` services. + */ +interface RadarAddedServices { + parser: { + TokenBuilder: RadarTokenBuilder; + ValueConverter: CommonValueConverter; + }; +} + +/** + * Union of Langium default services and `Radar` services. + */ +export type RadarServices = LangiumCoreServices & RadarAddedServices; + +/** + * Dependency injection module that overrides Langium default services and + * contributes the declared `Radar` services. + */ +export const RadarModule: Module = { + parser: { + TokenBuilder: () => new RadarTokenBuilder(), + 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 createRadarServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): { + shared: LangiumSharedCoreServices; + Radar: RadarServices; +} { + const shared: LangiumSharedCoreServices = inject( + createDefaultSharedCoreModule(context), + MermaidGeneratedSharedModule + ); + const Radar: RadarServices = inject( + createDefaultCoreModule({ shared }), + RadarGeneratedModule, + RadarModule + ); + shared.ServiceRegistry.register(Radar); + return { shared, Radar }; +} diff --git a/packages/parser/src/language/radar/radar.langium b/packages/parser/src/language/radar/radar.langium new file mode 100644 index 000000000..f0ecd13cd --- /dev/null +++ b/packages/parser/src/language/radar/radar.langium @@ -0,0 +1,89 @@ +grammar Radar +// import "../common/common"; +// Note: The import statement breaks TitleAndAccessibilities probably because of terminal order definition +// TODO: May need to change the common.langium to fix this + +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]*/; + +entry Radar: + NEWLINE* + ('radar-beta' | 'radar-beta:' | 'radar-beta' ':') + NEWLINE* + ( + TitleAndAccessibilities + | 'axis' axes+=Axis (',' axes+=Axis)* + | 'curve' curves+=Curve (',' curves+=Curve)* + | options+=Option (',' options+=Option)* + | NEWLINE + )* +; + +fragment Label: + '[' label=STRING ']' +; + +Axis: + name=ID (Label)? +; + +Curve: + name=ID (Label)? '{' Entries '}' +; + +fragment Entries: + NEWLINE* entries+=NumberEntry (',' NEWLINE* entries+=NumberEntry)* NEWLINE* | + NEWLINE* entries+=DetailedEntry (',' NEWLINE* entries+=DetailedEntry)* NEWLINE* +; + +interface Entry { + axis?: @Axis; + value: number; +} +DetailedEntry returns Entry: + axis=[Axis:ID] ':'? value=NUMBER +; +NumberEntry returns Entry: + value=NUMBER +; + +Option: + ( + name='showLegend' value=BOOLEAN + | name='ticks' value=NUMBER + | name='max' value=NUMBER + | name='min' value=NUMBER + | name='graticule' value=GRATICULE + ) +; + + +terminal NUMBER returns number: /(0|[1-9][0-9]*)(\.[0-9]+)?/; + +terminal BOOLEAN returns boolean: 'true' | 'false'; + +terminal GRATICULE returns string: 'circle' | 'polygon'; + +terminal ID returns string: /[a-zA-Z_][a-zA-Z0-9\-_]*/; +terminal STRING: /"[^"]*"|'[^']*'/; \ No newline at end of file diff --git a/packages/parser/src/language/radar/tokenBuilder.ts b/packages/parser/src/language/radar/tokenBuilder.ts new file mode 100644 index 000000000..b016fb5d8 --- /dev/null +++ b/packages/parser/src/language/radar/tokenBuilder.ts @@ -0,0 +1,7 @@ +import { AbstractMermaidTokenBuilder } from '../common/index.js'; + +export class RadarTokenBuilder extends AbstractMermaidTokenBuilder { + public constructor() { + super(['radar-beta']); + } +} diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 86713c2f1..020a86f7b 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -1,8 +1,8 @@ import type { LangiumParser, ParseResult } from 'langium'; -import type { Info, Packet, Pie, Architecture, GitGraph } from './index.js'; +import type { Info, Packet, Pie, Architecture, GitGraph, Radar } from './index.js'; -export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph; +export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar; const parsers: Record = {}; const initializers = { @@ -31,6 +31,11 @@ const initializers = { const parser = createGitGraphServices().GitGraph.parser.LangiumParser; parsers.gitGraph = parser; }, + radar: async () => { + const { createRadarServices } = await import('./language/radar/index.js'); + const parser = createRadarServices().Radar.parser.LangiumParser; + parsers.radar = parser; + }, } as const; export async function parse(diagramType: 'info', text: string): Promise; @@ -38,6 +43,7 @@ export async function parse(diagramType: 'packet', text: string): Promise; export async function parse(diagramType: 'architecture', text: string): Promise; export async function parse(diagramType: 'gitGraph', text: string): Promise; +export async function parse(diagramType: 'radar', text: string): Promise; export async function parse( diagramType: keyof typeof initializers, diff --git a/packages/parser/tests/packet.test.ts b/packages/parser/tests/packet.test.ts new file mode 100644 index 000000000..eb2ea303d --- /dev/null +++ b/packages/parser/tests/packet.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; + +import { Packet } from '../src/language/index.js'; +import { expectNoErrorsOrAlternatives, packetParse as parse } from './test-util.js'; + +describe('packet', () => { + it.each([ + `packet-beta`, + ` packet-beta `, + `\tpacket-beta\t`, + ` + \tpacket-beta + `, + ])('should handle regular packet', (context: string) => { + const result = parse(context); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Packet); + }); +}); diff --git a/packages/parser/tests/radar.test.ts b/packages/parser/tests/radar.test.ts new file mode 100644 index 000000000..5d483d53c --- /dev/null +++ b/packages/parser/tests/radar.test.ts @@ -0,0 +1,343 @@ +import { describe, expect, it } from 'vitest'; + +import { Radar } from '../src/language/index.js'; +import { expectNoErrorsOrAlternatives, radarParse as parse } from './test-util.js'; + +const mutateGlobalSpacing = (context: string) => { + return [ + context, + ` ${context} `, + `\t${context}\t`, + ` + \t${context} + `, + ]; +}; + +describe('radar', () => { + it.each([ + ...mutateGlobalSpacing('radar-beta'), + ...mutateGlobalSpacing('radar-beta:'), + ...mutateGlobalSpacing('radar-beta :'), + ])('should handle regular radar', (context: string) => { + const result = parse(context); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + }); + + describe('should handle title, accDescr, and accTitle', () => { + it.each([ + ...mutateGlobalSpacing(' title My Title'), + ...mutateGlobalSpacing('\n title My Title'), + ])('should handle title', (context: string) => { + const result = parse(`radar-beta${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { title } = result.value; + expect(title).toBe('My Title'); + }); + + it.each([ + ...mutateGlobalSpacing(' accDescr: My Accessible Description'), + ...mutateGlobalSpacing('\n accDescr: My Accessible Description'), + ])('should handle accDescr', (context: string) => { + const result = parse(`radar-beta${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { accDescr } = result.value; + expect(accDescr).toBe('My Accessible Description'); + }); + + it.each([ + ...mutateGlobalSpacing(' accTitle: My Accessible Title'), + ...mutateGlobalSpacing('\n accTitle: My Accessible Title'), + ])('should handle accTitle', (context: string) => { + const result = parse(`radar-beta${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { accTitle } = result.value; + expect(accTitle).toBe('My Accessible Title'); + }); + + it.each([ + ...mutateGlobalSpacing( + ' title My Title\n accDescr: My Accessible Description\n accTitle: My Accessible Title' + ), + ...mutateGlobalSpacing( + '\n title My Title\n accDescr: My Accessible Description\n accTitle: My Accessible Title' + ), + ])('should handle title + accDescr + accTitle', (context: string) => { + const result = parse(`radar-beta${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { title, accDescr, accTitle } = result.value; + expect(title).toBe('My Title'); + expect(accDescr).toBe('My Accessible Description'); + expect(accTitle).toBe('My Accessible Title'); + }); + }); + + describe('should handle axis', () => { + it.each([`axis my-axis`, `axis my-axis["My Axis Label"]`])( + 'should handle one axis', + (context: string) => { + const result = parse(`radar-beta\n${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { axes } = result.value; + expect(axes).toHaveLength(1); + expect(axes[0].$type).toBe('Axis'); + expect(axes[0].name).toBe('my-axis'); + } + ); + + it.each([ + `axis my-axis["My Axis Label"] + axis my-axis2`, + `axis my-axis, my-axis2`, + `axis my-axis["My Axis Label"], my-axis2`, + `axis my-axis, my-axis2["My Second Axis Label"]`, + ])('should handle multiple axes', (context: string) => { + const result = parse(`radar-beta\n${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { axes } = result.value; + expect(axes).toHaveLength(2); + expect(axes.every((axis) => axis.$type === 'Axis')).toBe(true); + expect(axes[0].name).toBe('my-axis'); + expect(axes[1].name).toBe('my-axis2'); + }); + + it.each([ + `axis my-axis["My Axis Label"] + axis my-axis2["My Second Axis Label"]`, + `axis my-axis ["My Axis Label"], my-axis2\t["My Second Axis Label"]`, + ])('should handle axis labels', (context: string) => { + const result = parse(`radar-beta\n${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { axes } = result.value; + expect(axes).toHaveLength(2); + expect(axes[0].name).toBe('my-axis'); + expect(axes[0].label).toBe('My Axis Label'); + expect(axes[1].name).toBe('my-axis2'); + expect(axes[1].label).toBe('My Second Axis Label'); + }); + + it('should not allow empty axis names', () => { + const result = parse(`radar-beta + axis`); + expect(result.parserErrors).not.toHaveLength(0); + }); + + it('should not allow non-comma separated axis names', () => { + const result = parse(`radar-beta + axis my-axis my-axis2`); + expect(result.parserErrors).not.toHaveLength(0); + }); + }); + + describe('should handle curves', () => { + it.each([ + `radar-beta + curve my-curve`, + `radar-beta + curve my-curve["My Curve Label"]`, + ])('should not allow curves without axes', (context: string) => { + const result = parse(`radar-beta${context}`); + expect(result.parserErrors).not.toHaveLength(0); + }); + + it.each([ + `radar-beta + axis my-axis + curve my-curve`, + `radar-beta + axis my-axis + curve my-curve["My Curve Label"]`, + ])('should not allow curves without entries', (context: string) => { + const result = parse(`radar-beta${context}`); + expect(result.parserErrors).not.toHaveLength(0); + }); + + it.each([ + `curve my-curve { 1 }`, + `curve my-curve { + 1 + }`, + `curve my-curve { + + 1 + + }`, + ])('should handle one curve with one entry', (context: string) => { + const result = parse(`radar-beta\naxis my-axis\n${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { curves } = result.value; + expect(curves).toHaveLength(1); + expect(curves[0].$type).toBe('Curve'); + expect(curves[0].name).toBe('my-curve'); + expect(curves[0].entries).toHaveLength(1); + expect(curves[0].entries[0].$type).toBe('Entry'); + expect(curves[0].entries[0].value).toBe(1); + }); + + it.each([ + `curve my-curve { my-axis 1 }`, + `curve my-curve { my-axis : 1 }`, + `curve my-curve { + my-axis: 1 + }`, + ])('should handle one curve with one detailed entry', (context: string) => { + const result = parse(`radar-beta\naxis my-axis\n${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { curves } = result.value; + expect(curves).toHaveLength(1); + expect(curves[0].$type).toBe('Curve'); + expect(curves[0].name).toBe('my-curve'); + expect(curves[0].entries).toHaveLength(1); + expect(curves[0].entries[0].$type).toBe('Entry'); + expect(curves[0].entries[0].value).toBe(1); + expect(curves[0].entries[0]?.axis?.$refText).toBe('my-axis'); + }); + + it.each([ + `curve my-curve { ax1 1, ax2 2 }`, + `curve my-curve { + ax1 1, + ax2 2 + }`, + `curve my-curve["My Curve Label"] { + ax1: 1, ax2: 2 + }`, + ])('should handle one curve with multiple detailed entries', (context: string) => { + const result = parse(`radar-beta\naxis ax1, ax1\n${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { curves } = result.value; + expect(curves).toHaveLength(1); + expect(curves[0].$type).toBe('Curve'); + expect(curves[0].name).toBe('my-curve'); + expect(curves[0].entries).toHaveLength(2); + expect(curves[0].entries[0].$type).toBe('Entry'); + expect(curves[0].entries[0].value).toBe(1); + expect(curves[0].entries[0]?.axis?.$refText).toBe('ax1'); + expect(curves[0].entries[1].$type).toBe('Entry'); + expect(curves[0].entries[1].value).toBe(2); + expect(curves[0].entries[1]?.axis?.$refText).toBe('ax2'); + }); + + it.each([ + `curve c1 { ax1 1, ax2 2 } + curve c2 { ax1 3, ax2 4 }`, + `curve c1 { + ax1 1, + ax2 2 + } + curve c2 { + ax1 3, + ax2 4 + }`, + `curve c1{ 1, 2 }, c2{ 3, 4 }`, + ])('should handle multiple curves', (context: string) => { + const result = parse(`radar-beta\naxis ax1, ax1\n${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { curves } = result.value; + expect(curves).toHaveLength(2); + expect(curves.every((curve) => curve.$type === 'Curve')).toBe(true); + expect(curves[0].name).toBe('c1'); + expect(curves[1].name).toBe('c2'); + }); + + it('should not allow empty curve names', () => { + const result = parse(`radar-beta + axis my-axis + curve`); + expect(result.parserErrors).not.toHaveLength(0); + }); + + it('should not allow number and detailed entries in the same curve', () => { + const result = parse(`radar-beta + axis ax1, ax2 + curve my-curve { 1, ax1 2 }`); + expect(result.parserErrors).not.toHaveLength(0); + }); + + it('should not allow non-comma separated entries', () => { + const result = parse(`radar-beta + axis ax1, ax2 + curve my-curve { ax1 1 ax2 2 }`); + expect(result.parserErrors).not.toHaveLength(0); + }); + }); + + describe('should handle options', () => { + it.each([`ticks 5`, `min 50`, `max 50`])( + `should handle number option %s`, + (context: string) => { + const result = parse(`radar-beta + axis ax1, ax2 + curve c1 { ax1 1, ax2 2 } + ${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { options } = result.value; + expect(options).toBeDefined(); + const option = options.find((option) => option.name === context.split(' ')[0]); + expect(option).toBeDefined(); + expect(option?.value).toBe(Number(context.split(' ')[1])); + } + ); + + it.each([`graticule circle`, `graticule polygon`])( + `should handle string option %s`, + (context: string) => { + const result = parse(`radar-beta + axis ax1, ax2 + curve c1 { ax1 1, ax2 2 } + ${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { options } = result.value; + expect(options).toBeDefined(); + const option = options.find((option) => option.name === context.split(' ')[0]); + expect(option).toBeDefined(); + expect(option?.value).toBe(context.split(' ')[1]); + } + ); + + it.each([`showLegend true`, `showLegend false`])( + `should handle boolean option %s`, + (context: string) => { + const result = parse(`radar-beta + axis ax1, ax2 + curve c1 { ax1 1, ax2 2 } + ${context}`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe(Radar); + + const { options } = result.value; + expect(options).toBeDefined(); + const option = options.find((option) => option.name === context.split(' ')[0]); + expect(option).toBeDefined(); + expect(option?.value).toBe(context.split(' ')[1] === 'true'); + } + ); + }); +}); diff --git a/packages/parser/tests/test-util.ts b/packages/parser/tests/test-util.ts index 5cb487758..bc2224e65 100644 --- a/packages/parser/tests/test-util.ts +++ b/packages/parser/tests/test-util.ts @@ -5,12 +5,18 @@ import type { InfoServices, Pie, PieServices, + Radar, + RadarServices, + Packet, + PacketServices, GitGraph, GitGraphServices, } from '../src/language/index.js'; import { createInfoServices, createPieServices, + createRadarServices, + createPacketServices, createGitGraphServices, } from '../src/language/index.js'; @@ -52,6 +58,28 @@ export function createPieTestServices() { } export const pieParse = createPieTestServices().parse; +const packetServices: PacketServices = createPacketServices().Packet; +const packetParser: LangiumParser = packetServices.parser.LangiumParser; +export function createPacketTestServices() { + const parse = (input: string) => { + return packetParser.parse(input); + }; + + return { services: packetServices, parse }; +} +export const packetParse = createPacketTestServices().parse; + +const radarServices: RadarServices = createRadarServices().Radar; +const radarParser: LangiumParser = radarServices.parser.LangiumParser; +export function createRadarTestServices() { + const parse = (input: string) => { + return radarParser.parse(input); + }; + + return { services: radarServices, parse }; +} +export const radarParse = createRadarTestServices().parse; + const gitGraphServices: GitGraphServices = createGitGraphServices().GitGraph; const gitGraphParser: LangiumParser = gitGraphServices.parser.LangiumParser; export function createGitGraphTestServices() { From 1fb91d14c96fca6f0a10cfc90ea0b8bcba6a47c8 Mon Sep 17 00:00:00 2001 From: Thomas Di Cizerone Date: Sun, 16 Mar 2025 18:13:50 +0100 Subject: [PATCH 093/177] =?UTF-8?q?=F0=9F=93=8A=20Add=20radar=20chart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/community/contributing.md | 16 ++ .../defaultConfig/variables/configKeys.md | 2 +- .../setup/mermaid/interfaces/MermaidConfig.md | 18 +- docs/syntax/radar.md | 253 ++++++++++++++++++ packages/mermaid/src/config.type.ts | 45 ++++ packages/mermaid/src/defaultConfig.ts | 4 + .../src/diagram-api/diagram-orchestration.ts | 4 +- packages/mermaid/src/diagrams/radar/db.ts | 128 +++++++++ .../mermaid/src/diagrams/radar/detector.ts | 22 ++ .../mermaid/src/diagrams/radar/diagram.ts | 12 + packages/mermaid/src/diagrams/radar/parser.ts | 23 ++ .../mermaid/src/diagrams/radar/radar.spec.ts | 167 ++++++++++++ .../mermaid/src/diagrams/radar/renderer.ts | 226 ++++++++++++++++ packages/mermaid/src/diagrams/radar/styles.ts | 71 +++++ packages/mermaid/src/diagrams/radar/types.ts | 47 ++++ packages/mermaid/src/mermaidAPI.spec.ts | 2 + .../mermaid/src/schemas/config.schema.yaml | 57 ++++ packages/mermaid/src/styles.spec.ts | 2 + 18 files changed, 1092 insertions(+), 7 deletions(-) create mode 100644 docs/syntax/radar.md create mode 100644 packages/mermaid/src/diagrams/radar/db.ts create mode 100644 packages/mermaid/src/diagrams/radar/detector.ts create mode 100644 packages/mermaid/src/diagrams/radar/diagram.ts create mode 100644 packages/mermaid/src/diagrams/radar/parser.ts create mode 100644 packages/mermaid/src/diagrams/radar/radar.spec.ts create mode 100644 packages/mermaid/src/diagrams/radar/renderer.ts create mode 100644 packages/mermaid/src/diagrams/radar/styles.ts create mode 100644 packages/mermaid/src/diagrams/radar/types.ts diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 792c90a98..b545d03b0 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -239,6 +239,22 @@ Code is the heart of every software project. We strive to make it better. Who if The core of Mermaid is located under `packages/mermaid/src`. +### Building Mermaid Locally + +**Host** + +```bash +pnpm run build +``` + +**Docker** + +```bash +./run build +``` + +This will build the Mermaid library and the documentation site. + ### Running Mermaid Locally **Host** diff --git a/docs/config/setup/defaultConfig/variables/configKeys.md b/docs/config/setup/defaultConfig/variables/configKeys.md index 821b7aec6..4687ad8bc 100644 --- a/docs/config/setup/defaultConfig/variables/configKeys.md +++ b/docs/config/setup/defaultConfig/variables/configKeys.md @@ -12,4 +12,4 @@ > `const` **configKeys**: `Set`<`string`> -Defined in: [packages/mermaid/src/defaultConfig.ts:270](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L270) +Defined in: [packages/mermaid/src/defaultConfig.ts:274](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L274) diff --git a/docs/config/setup/mermaid/interfaces/MermaidConfig.md b/docs/config/setup/mermaid/interfaces/MermaidConfig.md index 7734e135b..d08533713 100644 --- a/docs/config/setup/mermaid/interfaces/MermaidConfig.md +++ b/docs/config/setup/mermaid/interfaces/MermaidConfig.md @@ -105,7 +105,7 @@ You can set this attribute to base the seed on a static string. > `optional` **dompurifyConfig**: `Config` -Defined in: [packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202) +Defined in: [packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203) --- @@ -167,7 +167,7 @@ See > `optional` **fontSize**: `number` -Defined in: [packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204) +Defined in: [packages/mermaid/src/config.type.ts:205](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L205) --- @@ -280,7 +280,7 @@ Defines which main look to use for the diagram. > `optional` **markdownAutoWrap**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:205](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L205) +Defined in: [packages/mermaid/src/config.type.ts:206](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L206) --- @@ -336,6 +336,14 @@ Defined in: [packages/mermaid/src/config.type.ts:191](https://github.com/mermaid --- +### radar? + +> `optional` **radar**: `RadarDiagramConfig` + +Defined in: [packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202) + +--- + ### requirement? > `optional` **requirement**: `RequirementDiagramConfig` @@ -404,7 +412,7 @@ Defined in: [packages/mermaid/src/config.type.ts:188](https://github.com/mermaid > `optional` **suppressErrorRendering**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:211](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L211) +Defined in: [packages/mermaid/src/config.type.ts:212](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L212) Suppresses inserting 'Syntax error' diagram in the DOM. This is useful when you want to control how to handle syntax errors in your application. @@ -450,7 +458,7 @@ Defined in: [packages/mermaid/src/config.type.ts:186](https://github.com/mermaid > `optional` **wrap**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203) +Defined in: [packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204) --- diff --git a/docs/syntax/radar.md b/docs/syntax/radar.md new file mode 100644 index 000000000..5fade0804 --- /dev/null +++ b/docs/syntax/radar.md @@ -0,0 +1,253 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/radar.md](../../packages/mermaid/src/docs/syntax/radar.md). + +# Radar Diagram (v\+) + +## Introduction + +A radar diagram is a simple way to plot low-dimensional data in a circular format. + +It is also known as a **radar chart**, **spider chart**, **star chart**, **cobweb chart**, **polar chart**, or **Kiviat diagram**. + +## Usage + +This diagram type is particularly useful for developers, data scientists, and engineers who require a clear and concise way to represent data in a circular format. + +It is commonly used to graphically summarize and compare the performance of multiple entities across multiple dimensions. + +## Syntax + +```md +radar-beta +axis A, B, C, D, E +curve c1{1,2,3,4,5} +curve c2{5,4,3,2,1} +... More Fields ... +``` + +## Examples + +```mermaid-example +--- +title: "Grades" +--- +radar-beta + axis m["Math"], s["Science"], e["English"] + axis h["History"], g["Geography"], a["Art"] + curve a["Alice"]{85, 90, 80, 70, 75, 90} + curve b["Bob"]{70, 75, 85, 80, 90, 85} + + max 100 + min 0 +``` + +```mermaid +--- +title: "Grades" +--- +radar-beta + axis m["Math"], s["Science"], e["English"] + axis h["History"], g["Geography"], a["Art"] + curve a["Alice"]{85, 90, 80, 70, 75, 90} + curve b["Bob"]{70, 75, 85, 80, 90, 85} + + max 100 + min 0 +``` + +```mermaid-example +radar-beta + title Restaurant Comparison + axis food["Food Quality"], service["Service"], price["Price"] + axis ambiance["Ambiance"], + + curve a["Restaurant A"]{4, 3, 2, 4} + curve b["Restaurant B"]{3, 4, 3, 3} + curve c["Restaurant C"]{2, 3, 4, 2} + curve d["Restaurant D"]{2, 2, 4, 3} + + graticule polygon + max 5 + +``` + +```mermaid +radar-beta + title Restaurant Comparison + axis food["Food Quality"], service["Service"], price["Price"] + axis ambiance["Ambiance"], + + curve a["Restaurant A"]{4, 3, 2, 4} + curve b["Restaurant B"]{3, 4, 3, 3} + curve c["Restaurant C"]{2, 3, 4, 2} + curve d["Restaurant D"]{2, 2, 4, 3} + + graticule polygon + max 5 + +``` + +## Details of Syntax + +### Title + +`title`: The title is an optional field that allows to render a title at the top of the radar diagram. + +``` +radar-beta + title Title of the Radar Diagram + ... +``` + +### Axis + +`axis`: The axis keyword is used to define the axes of the radar diagram. + +Each axis is represented by an ID and an optional label. + +Multiple axes can be defined in a single line. + +``` +radar-beta + axis id1["Label1"] + axis id2["Label2"], id3["Label3"] + ... +``` + +### Curve + +`curve`: The curve keyword is used to define the data points for a curve in the radar diagram. + +Each curve is represented by an ID, an optional label, and a list of values. + +Values can be defined by a list of numbers or a list of key-value pairs. If key-value pairs are used, the key represents the axis ID and the value represents the data point. Else, the data points are assumed to be in the order of the axes defined. + +Multiple curves can be defined in a single line. + +``` +radar-beta + axis axis1, axis2, axis3 + curve id1["Label1"]{1, 2, 3} + curve id2["Label2"]{4, 5, 6}, id3{7, 8, 9} + curve id4{ axis3: 30, axis1: 20, axis2: 10 } + ... +``` + +### Options + +- `showLegend`: The showLegend keyword is used to show or hide the legend in the radar diagram. The legend is shown by default. +- `max`: The maximum value for the radar diagram. This is used to scale the radar diagram. If not provided, the maximum value is calculated from the data points. +- `min`: The minimum value for the radar diagram. This is used to scale the radar diagram. If not provided, the minimum value is `0`. +- `graticule`: The graticule keyword is used to define the type of graticule to be rendered in the radar diagram. The graticule can be `circle` or `polygon`. If not provided, the default graticule is `circle`. +- `ticks`: The ticks keyword is used to define the number of ticks on the graticule. It is the number of concentric circles or polygons drawn to indicate the scale of the radar diagram. If not provided, the default number of ticks is `5`. + +``` +radar-beta + ... + showLegend true + max 100 + min 0 + graticule circle + ticks 5 + ... +``` + +## Configuration + +Please refer to the [configuration](/config/schema-docs/config-defs-radar-diagram-config.html) guide for details. + +| Parameter | Description | Default Value | +| --------------- | ---------------------------------------- | ------------- | +| width | Width of the radar diagram | `600` | +| height | Height of the radar diagram | `600` | +| marginTop | Top margin of the radar diagram | `50` | +| marginBottom | Bottom margin of the radar diagram | `50` | +| marginLeft | Left margin of the radar diagram | `50` | +| marginRight | Right margin of the radar diagram | `50` | +| axisScaleFactor | Scale factor for the axis | `1` | +| axisLabelFactor | Factor to adjust the axis label position | `1.05` | +| curveTension | Tension for the rounded curves | `0.17` | + +## Theme Variables + +### Global Theme Variables + +> **Note** +> The default values for these variables depend on the theme used. To override the default values, set the desired values in the themeVariables section of the configuration: +> %%{init: {"themeVariables": {"cScale0": "#FF0000", "cScale1": "#00FF00"}} }%% + +Radar charts support the color scales `cScale${i}` where `i` is a number from `0` to the theme's maximum number of colors in its color scale. Usually, the maximum number of colors is `12`. + +| Property | Description | +| ---------- | ------------------------------ | +| fontSize | Font size of the title | +| titleColor | Color of the title | +| cScale${i} | Color scale for the i-th curve | + +### Radar Style Options + +> **Note** +> Specific variables for radar resides inside the `radar` key. To set the radar style options, use this syntax. +> %%{init: {"themeVariables": {"radar": {"axisColor": "#FF0000"}} } }%% + +| Property | Description | Default Value | +| -------------------- | ---------------------------- | ------------- | +| axisColor | Color of the axis lines | `black` | +| axisStrokeWidth | Width of the axis lines | `1` | +| axisLabelFontSize | Font size of the axis labels | `12px` | +| curveOpacity | Opacity of the curves | `0.7` | +| curveStrokeWidth | Width of the curves | `2` | +| graticuleColor | Color of the graticule | `black` | +| graticuleOpacity | Opacity of the graticule | `0.5` | +| graticuleStrokeWidth | Width of the graticule | `1` | +| legendBoxSize | Size of the legend box | `10` | +| legendFontSize | Font size of the legend | `14px` | + +## Example on config and theme + +```mermaid-example +--- +config: + radar: + axisScaleFactor: 0.25 + curveTension: 0.1 + theme: base + themeVariables: + cScale0: "#FF0000" + cScale1: "#00FF00" + cScale2: "#0000FF" + radar: + curveOpacity: 0 +--- +radar-beta + axis A, B, C, D, E + curve c1{1,2,3,4,5} + curve c2{5,4,3,2,1} + curve c3{3,3,3,3,3} +``` + +```mermaid +--- +config: + radar: + axisScaleFactor: 0.25 + curveTension: 0.1 + theme: base + themeVariables: + cScale0: "#FF0000" + cScale1: "#00FF00" + cScale2: "#0000FF" + radar: + curveOpacity: 0 +--- +radar-beta + axis A, B, C, D, E + curve c1{1,2,3,4,5} + curve c2{5,4,3,2,1} + curve c3{3,3,3,3,3} +``` + + diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 60a1fc2c1..c02a41a1c 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -199,6 +199,7 @@ export interface MermaidConfig { sankey?: SankeyDiagramConfig; packet?: PacketDiagramConfig; block?: BlockDiagramConfig; + radar?: RadarDiagramConfig; dompurifyConfig?: DOMPurifyConfiguration; wrap?: boolean; fontSize?: number; @@ -1526,6 +1527,50 @@ export interface PacketDiagramConfig extends BaseDiagramConfig { export interface BlockDiagramConfig extends BaseDiagramConfig { padding?: number; } +/** + * The object containing configurations specific for radar diagrams. + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "RadarDiagramConfig". + */ +export interface RadarDiagramConfig extends BaseDiagramConfig { + /** + * The size of the radar diagram. + */ + width?: number; + /** + * The size of the radar diagram. + */ + height?: number; + /** + * The margin from the top of the radar diagram. + */ + marginTop?: number; + /** + * The margin from the right of the radar diagram. + */ + marginRight?: number; + /** + * The margin from the bottom of the radar diagram. + */ + marginBottom?: number; + /** + * The margin from the left of the radar diagram. + */ + marginLeft?: number; + /** + * The scale factor of the axis. + */ + axisScaleFactor?: number; + /** + * The scale factor of the axis label. + */ + axisLabelFactor?: number; + /** + * The tension factor for the Catmull-Rom spline conversion to cubic Bézier curves. + */ + curveTension?: number; +} /** * This interface was referenced by `MermaidConfig`'s JSON-Schema * via the `definition` "FontConfig". diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index a3dab2ddb..2e4e20f50 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -255,8 +255,12 @@ const config: RequiredDeep = { packet: { ...defaultConfigJson.packet, }, + radar: { + ...defaultConfigJson.radar, + }, }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any const keyify = (obj: any, prefix = ''): string[] => Object.keys(obj).reduce((res: string[], el): string[] => { if (Array.isArray(obj[el])) { diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 5b8cfc3fe..8f2b76abb 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -22,6 +22,7 @@ import mindmap from '../diagrams/mindmap/detector.js'; import kanban from '../diagrams/kanban/detector.js'; import sankey from '../diagrams/sankey/sankeyDetector.js'; import { packet } from '../diagrams/packet/detector.js'; +import { radar } from '../diagrams/radar/detector.js'; import block from '../diagrams/block/blockDetector.js'; import architecture from '../diagrams/architecture/architectureDetector.js'; import { registerLazyLoadedDiagrams } from './detectType.js'; @@ -94,6 +95,7 @@ export const addDiagrams = () => { packet, xychart, block, - architecture + architecture, + radar ); }; diff --git a/packages/mermaid/src/diagrams/radar/db.ts b/packages/mermaid/src/diagrams/radar/db.ts new file mode 100644 index 000000000..ff95a3be0 --- /dev/null +++ b/packages/mermaid/src/diagrams/radar/db.ts @@ -0,0 +1,128 @@ +import { getConfig as commonGetConfig } from '../../config.js'; +import type { RadarDiagramConfig } 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 { + Axis, + Curve, + Option, + Entry, +} from '../../../../parser/dist/src/language/generated/ast.js'; +import type { RadarAxis, RadarCurve, RadarOptions, RadarDB, RadarData } from './types.js'; + +const defaultOptions: RadarOptions = { + showLegend: true, + ticks: 5, + max: null, + min: 0, + graticule: 'circle', +}; + +const defaultRadarData: RadarData = { + axes: [], + curves: [], + options: defaultOptions, +}; + +let data: RadarData = structuredClone(defaultRadarData); + +const DEFAULT_RADAR_CONFIG: Required = DEFAULT_CONFIG.radar; + +const getConfig = (): Required => { + const config = cleanAndMerge({ + ...DEFAULT_RADAR_CONFIG, + ...commonGetConfig().radar, + }); + return config; +}; + +const getAxes = (): RadarAxis[] => data.axes; +const getCurves = (): RadarCurve[] => data.curves; +const getOptions = (): RadarOptions => data.options; + +const setAxes = (axes: Axis[]) => { + data.axes = axes.map((axis) => { + return { + name: axis.name, + label: axis.label ?? axis.name, + }; + }); +}; + +const setCurves = (curves: Curve[]) => { + data.curves = curves.map((curve) => { + return { + name: curve.name, + label: curve.label ?? curve.name, + entries: computeCurveEntries(curve.entries), + }; + }); +}; + +const computeCurveEntries = (entries: Entry[]): number[] => { + // If entries have axis reference, we must order them according to the axes + if (entries[0].axis == undefined) { + return entries.map((entry) => entry.value); + } + const axes = getAxes(); + if (axes.length === 0) { + throw new Error('Axes must be populated before curves for reference entries'); + } + return axes.map((axis) => { + const entry = entries.find((entry) => entry.axis?.$refText === axis.name); + if (entry === undefined) { + throw new Error('Missing entry for axis ' + axis.label); + } + return entry.value; + }); +}; + +const setOptions = (options: Option[]) => { + // Create a map from option names to option objects for quick lookup + const optionMap = options.reduce( + (acc, option) => { + acc[option.name] = option; + return acc; + }, + {} as Record + ); + + data.options = { + showLegend: (optionMap.showLegend?.value as boolean) ?? defaultOptions.showLegend, + ticks: (optionMap.ticks?.value as number) ?? defaultOptions.ticks, + max: (optionMap.max?.value as number) ?? defaultOptions.max, + min: (optionMap.min?.value as number) ?? defaultOptions.min, + graticule: (optionMap.graticule?.value as 'circle' | 'polygon') ?? defaultOptions.graticule, + }; +}; + +const clear = () => { + commonClear(); + data = structuredClone(defaultRadarData); +}; + +export const db: RadarDB = { + getAxes, + getCurves, + getOptions, + setAxes, + setCurves, + setOptions, + getConfig, + clear, + setAccTitle, + getAccTitle, + setDiagramTitle, + getDiagramTitle, + getAccDescription, + setAccDescription, +}; diff --git a/packages/mermaid/src/diagrams/radar/detector.ts b/packages/mermaid/src/diagrams/radar/detector.ts new file mode 100644 index 000000000..9c29d0f00 --- /dev/null +++ b/packages/mermaid/src/diagrams/radar/detector.ts @@ -0,0 +1,22 @@ +import type { + DiagramDetector, + DiagramLoader, + ExternalDiagramDefinition, +} from '../../diagram-api/types.js'; + +const id = 'radar'; + +const detector: DiagramDetector = (txt) => { + return /^\s*radar-beta/.test(txt); +}; + +const loader: DiagramLoader = async () => { + const { diagram } = await import('./diagram.js'); + return { id, diagram }; +}; + +export const radar: ExternalDiagramDefinition = { + id, + detector, + loader, +}; diff --git a/packages/mermaid/src/diagrams/radar/diagram.ts b/packages/mermaid/src/diagrams/radar/diagram.ts new file mode 100644 index 000000000..a73a77c05 --- /dev/null +++ b/packages/mermaid/src/diagrams/radar/diagram.ts @@ -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, +}; diff --git a/packages/mermaid/src/diagrams/radar/parser.ts b/packages/mermaid/src/diagrams/radar/parser.ts new file mode 100644 index 000000000..e16e9b3dd --- /dev/null +++ b/packages/mermaid/src/diagrams/radar/parser.ts @@ -0,0 +1,23 @@ +import type { Radar } 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'; + +const populate = (ast: Radar) => { + populateCommonDb(ast, db); + const { axes, curves, options } = ast; + // Here we can add specific logic between the AST and the DB + db.setAxes(axes); + db.setCurves(curves); + db.setOptions(options); +}; + +export const parser: ParserDefinition = { + parse: async (input: string): Promise => { + const ast: Radar = await parse('radar', input); + log.debug(ast); + populate(ast); + }, +}; diff --git a/packages/mermaid/src/diagrams/radar/radar.spec.ts b/packages/mermaid/src/diagrams/radar/radar.spec.ts new file mode 100644 index 000000000..9971e8b17 --- /dev/null +++ b/packages/mermaid/src/diagrams/radar/radar.spec.ts @@ -0,0 +1,167 @@ +import { it, describe, expect } from 'vitest'; +import { db } from './db.js'; +import { parser } from './parser.js'; + +const { + clear, + getDiagramTitle, + getAccTitle, + getAccDescription, + getAxes, + getCurves, + getOptions, + getConfig, +} = db; + +describe('radar diagrams', () => { + beforeEach(() => { + clear(); + }); + + it('should handle a simple radar definition', async () => { + const str = `radar-beta + axis A,B,C + curve mycurve{1,2,3}`; + await expect(parser.parse(str)).resolves.not.toThrow(); + }); + + it('should handle diagram with data and title', async () => { + const str = `radar-beta + title Radar diagram + accTitle: Radar accTitle + accDescr: Radar accDescription + axis A["Axis A"], B["Axis B"] ,C["Axis C"] + curve mycurve["My Curve"]{1,2,3} + `; + await expect(parser.parse(str)).resolves.not.toThrow(); + expect(getDiagramTitle()).toMatchInlineSnapshot('"Radar diagram"'); + expect(getAccTitle()).toMatchInlineSnapshot('"Radar accTitle"'); + expect(getAccDescription()).toMatchInlineSnapshot('"Radar accDescription"'); + expect(getAxes()).toMatchInlineSnapshot(` + [ + { + "label": "Axis A", + "name": "A", + }, + { + "label": "Axis B", + "name": "B", + }, + { + "label": "Axis C", + "name": "C", + }, + ] + `); + expect(getCurves()).toMatchInlineSnapshot(` + [ + { + "entries": [ + 1, + 2, + 3, + ], + "label": "My Curve", + "name": "mycurve", + }, + ] + `); + expect(getOptions()).toMatchInlineSnapshot(` + { + "graticule": "circle", + "max": null, + "min": 0, + "showLegend": true, + "ticks": 5, + } + `); + }); + + it('should handle a radar diagram with options', async () => { + const str = `radar-beta + ticks 10 + showLegend false + graticule polygon + min 1 + max 10 + `; + await expect(parser.parse(str)).resolves.not.toThrow(); + expect(getOptions()).toMatchInlineSnapshot(` + { + "graticule": "polygon", + "max": 10, + "min": 1, + "showLegend": false, + "ticks": 10, + } + `); + }); + + it('should handle curve with detailed data in any order', async () => { + const str = `radar-beta + axis A,B,C + curve mycurve{ C: 3, A: 1, B: 2 }`; + await expect(parser.parse(str)).resolves.not.toThrow(); + expect(getCurves()).toMatchInlineSnapshot(` + [ + { + "entries": [ + 1, + 2, + 3, + ], + "label": "mycurve", + "name": "mycurve", + }, + ] + `); + }); + + it('should handle radar diagram with comments', async () => { + const str = `radar-beta + %% This is a comment + axis A,B,C + %% This is another comment + curve mycurve{1,2,3} + `; + await expect(parser.parse(str)).resolves.not.toThrow(); + }); + + it('should handle radar diagram with config override', async () => { + const str = ` + %%{init: {'radar': {'marginTop': 80, 'axisLabelFactor': 1.25}}}%% + radar-beta + axis A,B,C + curve mycurve{1,2,3} + `; + await expect(parser.parse(str)).resolves.not.toThrow(); + + // TODO: ✨ Fix this test + // expect(getConfig().marginTop).toBe(80); + // expect(getConfig().axisLabelFactor).toBe(1.25); + }); + + it('should parse radar diagram with theme override', async () => { + const str = ` + %%{init: { "theme": "base", "themeVariables": {'fontSize': 80, 'cScale0': '#123456' }}}%% + radar-beta: + axis A,B,C + curve mycurve{1,2,3} + `; + await expect(parser.parse(str)).resolves.not.toThrow(); + + // TODO: ✨ Add tests for theme override + }); + + it('should handle radar diagram with radar style override', async () => { + const str = ` + %%{init: { "theme": "base", "themeVariables": {'fontSize': 10, 'radar': { 'axisColor': '#FF0000' }}}}%% + radar-beta + axis A,B,C + curve mycurve{1,2,3} + `; + await expect(parser.parse(str)).resolves.not.toThrow(); + + // TODO: ✨ Add tests for style override + }); +}); diff --git a/packages/mermaid/src/diagrams/radar/renderer.ts b/packages/mermaid/src/diagrams/radar/renderer.ts new file mode 100644 index 000000000..5a5b47f45 --- /dev/null +++ b/packages/mermaid/src/diagrams/radar/renderer.ts @@ -0,0 +1,226 @@ +import type { Diagram } from '../../Diagram.js'; +import type { RadarDiagramConfig } from '../../config.type.js'; +import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import type { RadarDB, RadarAxis, RadarCurve } from './types.js'; + +const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => { + const db = diagram.db as RadarDB; + const axes = db.getAxes(); + const curves = db.getCurves(); + const options = db.getOptions(); + const config = db.getConfig(); + const title = db.getDiagramTitle(); + + const svg: SVG = selectSvgElement(id); + + // 🖼️ Draw the main frame + const g = drawFrame(svg, config); + + // The maximum value for the radar chart is the 'max' option if it exists, + // otherwise it is the maximum value of the curves + const maxValue: number = + options.max ?? Math.max(...curves.map((curve) => Math.max(...curve.entries))); + const minValue: number = options.min; + const radius = Math.min(config.width, config.height) / 2; + + // 🕸️ Draw graticule + drawGraticule(g, axes, radius, options.ticks, options.graticule); + + // 🪓 Draw the axes + drawAxes(g, axes, radius, config); + + // 📊 Draw the curves + drawCurves(g, axes, curves, minValue, maxValue, options.graticule, config); + + // 🏷 Draw Legend + drawLegend(g, curves, options.showLegend, config); + + // 🏷 Draw Title + g.append('text') + .attr('class', 'radarTitle') + .text(title) + .attr('x', 0) + .attr('y', -config.height / 2 - config.marginTop); +}; + +// Returns a g element to center the radar chart +// it is of type SVGElement +const drawFrame = (svg: SVG, config: Required): SVGGroup => { + const totalWidth = config.width + config.marginLeft + config.marginRight; + const totalHeight = config.height + config.marginTop + config.marginBottom; + const center = { + x: config.marginLeft + config.width / 2, + y: config.marginTop + config.height / 2, + }; + // Initialize the SVG + svg + .attr('viewbox', `0 0 ${totalWidth} ${totalHeight}`) + .attr('width', totalWidth) + .attr('height', totalHeight); + // g element to center the radar chart + return svg.append('g').attr('transform', `translate(${center.x}, ${center.y})`); +}; + +const drawGraticule = ( + g: SVGGroup, + axes: RadarAxis[], + radius: number, + ticks: number, + graticule: string +) => { + if (graticule === 'circle') { + // Draw a circle for each tick + for (let i = 0; i < ticks; i++) { + const r = (radius * (i + 1)) / ticks; + g.append('circle').attr('r', r).attr('class', 'radarGraticule'); + } + } else if (graticule === 'polygon') { + // Draw a polygon + const numAxes = axes.length; + for (let i = 0; i < ticks; i++) { + const r = (radius * (i + 1)) / ticks; + const points = axes + .map((_, j) => { + const angle = (2 * j * Math.PI) / numAxes - Math.PI / 2; + const x = r * Math.cos(angle); + const y = r * Math.sin(angle); + return `${x},${y}`; + }) + .join(' '); + g.append('polygon').attr('points', points).attr('class', 'radarGraticule'); + } + } +}; + +const drawAxes = ( + g: SVGGroup, + axes: RadarAxis[], + radius: number, + config: Required +) => { + const numAxes = axes.length; + + for (let i = 0; i < numAxes; i++) { + const label = axes[i].label; + const angle = (2 * i * Math.PI) / numAxes - Math.PI / 2; + g.append('line') + .attr('x1', 0) + .attr('y1', 0) + .attr('x2', radius * config.axisScaleFactor * Math.cos(angle)) + .attr('y2', radius * config.axisScaleFactor * Math.sin(angle)) + .attr('class', 'radarAxisLine'); + g.append('text') + .text(label) + .attr('x', radius * config.axisLabelFactor * Math.cos(angle)) + .attr('y', radius * config.axisLabelFactor * Math.sin(angle)) + .attr('class', 'radarAxisLabel'); + } +}; + +export const renderer: DiagramRenderer = { draw }; +function drawCurves( + g: SVGGroup, + axes: RadarAxis[], + curves: RadarCurve[], + minValue: number, + maxValue: number, + graticule: string, + config: Required +) { + const numAxes = axes.length; + const radius = Math.min(config.width, config.height) / 2; + + curves.forEach((curve, index) => { + if (curve.entries.length !== numAxes) { + // Skip curves that do not have an entry for each axis. + return; + } + // Compute points for the curve. + const points = curve.entries.map((entry, i) => { + const angle = (2 * Math.PI * i) / numAxes - Math.PI / 2; + const r = relativeRadius(entry, minValue, maxValue, radius); + const x = r * Math.cos(angle); + const y = r * Math.sin(angle); + return { x, y }; + }); + + if (graticule === 'circle') { + // Draw a closed curve through the points. + g.append('path') + .attr('d', closedRoundCurve(points, config.curveTension)) + .attr('class', `radarCurve-${index}`); + } else if (graticule === 'polygon') { + // Draw a polygon for each curve. + g.append('polygon') + .attr('points', points.map((p) => `${p.x},${p.y}`).join(' ')) + .attr('class', `radarCurve-${index}`); + } + }); +} + +function relativeRadius(value: number, minValue: number, maxValue: number, radius: number): number { + const clippedValue = Math.min(Math.max(value, minValue), maxValue); + return (radius * (clippedValue - minValue)) / (maxValue - minValue); +} + +function closedRoundCurve(points: { x: number; y: number }[], tension: number): string { + // Catmull-Rom spline helper function + const numPoints = points.length; + let d = `M${points[0].x},${points[0].y}`; + // For each segment from point i to point (i+1) mod n, compute control points. + for (let i = 0; i < numPoints; i++) { + const p0 = points[(i - 1 + numPoints) % numPoints]; + const p1 = points[i]; + const p2 = points[(i + 1) % numPoints]; + const p3 = points[(i + 2) % numPoints]; + // Calculate the control points for the cubic Bezier segment + const cp1 = { + x: p1.x + (p2.x - p0.x) * tension, + y: p1.y + (p2.y - p0.y) * tension, + }; + const cp2 = { + x: p2.x - (p3.x - p1.x) * tension, + y: p2.y - (p3.y - p1.y) * tension, + }; + d += ` C${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${p2.x},${p2.y}`; + } + return `${d} Z`; +} + +function drawLegend( + g: SVGGroup, + curves: RadarCurve[], + showLegend: boolean, + config: Required +) { + if (!showLegend) { + return; + } + + // Create a legend group and position it in the top-right corner of the chart. + const legendX = ((config.width / 2 + config.marginRight) * 3) / 4; + const legendY = (-(config.height / 2 + config.marginTop) * 3) / 4; + const lineHeight = 20; + + curves.forEach((curve, index) => { + const itemGroup = g + .append('g') + .attr('transform', `translate(${legendX}, ${legendY + index * lineHeight})`); + + // Draw a square marker for this curve. + itemGroup + .append('rect') + .attr('width', 12) + .attr('height', 12) + .attr('class', `radarLegendBox-${index}`); + + // Draw the label text next to the marker. + itemGroup + .append('text') + .attr('x', 16) + .attr('y', 0) + .attr('class', 'radarLegendText') + .text(curve.label); + }); +} diff --git a/packages/mermaid/src/diagrams/radar/styles.ts b/packages/mermaid/src/diagrams/radar/styles.ts new file mode 100644 index 000000000..591385507 --- /dev/null +++ b/packages/mermaid/src/diagrams/radar/styles.ts @@ -0,0 +1,71 @@ +import type { DiagramStylesProvider } from '../../diagram-api/types.js'; +import { cleanAndMerge } from '../../utils.js'; +import type { RadarStyleOptions } from './types.js'; +import { getThemeVariables } from '../../themes/theme-default.js'; +import { getConfig as getConfigAPI } from '../../config.js'; + +const genIndexStyles = ( + themeVariables: ReturnType, + radarOptions: RadarStyleOptions +) => { + let sections = ''; + for (let i = 0; i < themeVariables.THEME_COLOR_LIMIT; i++) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const indexColor = (themeVariables as any)[`cScale${i}`]; + sections += ` + .radarCurve-${i} { + color: ${indexColor}; + fill: ${indexColor}; + fill-opacity: ${radarOptions.curveOpacity}; + stroke: ${indexColor}; + stroke-width: ${radarOptions.curveStrokeWidth}; + } + .radarLegendBox-${i} { + fill: ${indexColor}; + fill-opacity: ${radarOptions.curveOpacity}; + stroke: ${indexColor}; + } + `; + } + return sections; +}; + +export const styles: DiagramStylesProvider = ({ radar }: { radar?: RadarStyleOptions } = {}) => { + const defaultThemeVariables = getThemeVariables(); + const currentConfig = getConfigAPI(); + + const themeVariables = cleanAndMerge(defaultThemeVariables, currentConfig.themeVariables); + const radarOptions: RadarStyleOptions = cleanAndMerge(themeVariables.radar, radar); + return ` + .radarTitle { + font-size: ${themeVariables.fontSize}; + text-color: ${themeVariables.titleColor}; + dominant-baseline: hanging; + text-anchor: middle; + } + .radarAxisLine { + stroke: ${radarOptions.axisColor}; + stroke-width: ${radarOptions.axisStrokeWidth}; + } + .radarAxisLabel { + dominant-baseline: middle; + text-anchor: middle; + font-size: ${radarOptions.axisLabelFontSize}px; + color: ${radarOptions.axisColor}; + } + .radarGraticule { + fill: ${radarOptions.graticuleColor}; + fill-opacity: ${radarOptions.graticuleOpacity}; + stroke: ${radarOptions.graticuleColor}; + stroke-width: ${radarOptions.graticuleStrokeWidth}; + } + .radarLegendText { + text-anchor: start; + font-size: ${radarOptions.legendFontSize}px; + dominant-baseline: hanging; + } + ${genIndexStyles(themeVariables, radarOptions)} + `; +}; + +export default styles; diff --git a/packages/mermaid/src/diagrams/radar/types.ts b/packages/mermaid/src/diagrams/radar/types.ts new file mode 100644 index 000000000..307fbaf04 --- /dev/null +++ b/packages/mermaid/src/diagrams/radar/types.ts @@ -0,0 +1,47 @@ +import type { Axis, Curve, Option } from '../../../../parser/dist/src/language/generated/ast.js'; +import type { RadarDiagramConfig } from '../../config.type.js'; +import type { DiagramDBBase } from '../../diagram-api/types.js'; + +export interface RadarAxis { + name: string; + label: string; +} +export interface RadarCurve { + name: string; + entries: number[]; + label: string; +} +export interface RadarOptions { + showLegend: boolean; + ticks: number; + max: number | null; + min: number; + graticule: 'circle' | 'polygon'; +} +export interface RadarDB extends DiagramDBBase { + getAxes: () => RadarAxis[]; + getCurves: () => RadarCurve[]; + getOptions: () => RadarOptions; + setAxes: (axes: Axis[]) => void; + setCurves: (curves: Curve[]) => void; + setOptions: (options: Option[]) => void; +} + +export interface RadarStyleOptions { + axisColor: string; + axisStrokeWidth: number; + axisLabelFontSize: number; + curveOpacity: number; + curveStrokeWidth: number; + graticuleColor: string; + graticuleOpacity: number; + graticuleStrokeWidth: number; + legendBoxSize: number; + legendFontSize: number; +} + +export interface RadarData { + axes: RadarAxis[]; + curves: RadarCurve[]; + options: RadarOptions; +} diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index c3480d203..64f4b8d60 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -30,6 +30,7 @@ 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/radar/renderer.js'); // ------------------------------------- @@ -797,6 +798,7 @@ graph TD;A--x|text including URL space|B;`) { textDiagramType: 'requirementDiagram', expectedType: 'requirement' }, { textDiagramType: 'sequenceDiagram', expectedType: 'sequence' }, { textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' }, + { textDiagramType: 'radar-beta', expectedType: 'radar' }, ]; describe('accessibility', () => { diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index e45dd44be..1445e2dd2 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -55,6 +55,7 @@ required: - packet - block - look + - radar properties: theme: description: | @@ -292,6 +293,8 @@ properties: $ref: '#/$defs/PacketDiagramConfig' block: $ref: '#/$defs/BlockDiagramConfig' + radar: + $ref: '#/$defs/RadarDiagramConfig' dompurifyConfig: title: DOM Purify Configuration description: Configuration options to pass to the `dompurify` library. @@ -2208,6 +2211,60 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) minimum: 0 default: 8 + RadarDiagramConfig: + title: Radar Diagram Config + allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }] + description: The object containing configurations specific for radar diagrams. + type: object + unevaluatedProperties: false + properties: + width: + description: The size of the radar diagram. + type: number + minimum: 1 + default: 600 + height: + description: The size of the radar diagram. + type: number + minimum: 1 + default: 600 + marginTop: + description: The margin from the top of the radar diagram. + type: number + minimum: 0 + default: 50 + marginRight: + description: The margin from the right of the radar diagram. + type: number + minimum: 0 + default: 50 + marginBottom: + description: The margin from the bottom of the radar diagram. + type: number + minimum: 0 + default: 50 + marginLeft: + description: The margin from the left of the radar diagram. + type: number + minimum: 0 + default: 50 + axisScaleFactor: + description: The scale factor of the axis. + type: number + minimum: 0 + default: 1 + axisLabelFactor: + description: The scale factor of the axis label. + type: number + minimum: 0 + default: 1.05 + curveTension: + description: The tension factor for the Catmull-Rom spline conversion to cubic Bézier curves. + type: number + minimum: 0 + maximum: 1 + default: 0.17 + FontCalculator: title: Font Calculator description: | diff --git a/packages/mermaid/src/styles.spec.ts b/packages/mermaid/src/styles.spec.ts index 70e9e7ec5..8c5263edc 100644 --- a/packages/mermaid/src/styles.spec.ts +++ b/packages/mermaid/src/styles.spec.ts @@ -29,6 +29,7 @@ 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 radar from './diagrams/radar/styles.js'; import themes from './themes/index.js'; function checkValidStylisCSSStyleSheet(stylisString: string) { @@ -99,6 +100,7 @@ describe('styles', () => { block, timeline, packet, + radar, })) { test(`should return a valid style for diagram ${diagramId} and theme ${themeId}`, async () => { const { default: getStyles, addStylesForDiagram } = await import('./styles.js'); From d53c66dde5797038dfa6ee1d6df3e419a00ac22b Mon Sep 17 00:00:00 2001 From: Thomas Di Cizerone Date: Sun, 16 Mar 2025 18:15:30 +0100 Subject: [PATCH 094/177] =?UTF-8?q?=F0=9F=8E=A8=20Add=20themes=20for=20rad?= =?UTF-8?q?ar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/mermaid/src/themes/theme-base.js | 14 ++++++++++++++ packages/mermaid/src/themes/theme-dark.js | 14 ++++++++++++++ packages/mermaid/src/themes/theme-default.js | 14 ++++++++++++++ packages/mermaid/src/themes/theme-forest.js | 14 ++++++++++++++ packages/mermaid/src/themes/theme-neutral.js | 14 ++++++++++++++ 5 files changed, 70 insertions(+) diff --git a/packages/mermaid/src/themes/theme-base.js b/packages/mermaid/src/themes/theme-base.js index 1f858275f..73ffef070 100644 --- a/packages/mermaid/src/themes/theme-base.js +++ b/packages/mermaid/src/themes/theme-base.js @@ -230,6 +230,20 @@ class Theme { this.pieOuterStrokeColor = this.pieOuterStrokeColor || 'black'; this.pieOpacity = this.pieOpacity || '0.7'; + /* radar */ + this.radar = { + axisColor: this.radar?.axisColor || this.lineColor, + axisStrokeWidth: this.radar?.axisStrokeWidth || 2, + axisLabelFontSize: this.radar?.axisLabelFontSize || 12, + curveOpacity: this.radar?.curveOpacity || 0.5, + curveStrokeWidth: this.radar?.curveStrokeWidth || 2, + graticuleColor: this.radar?.graticuleColor || '#DEDEDE', + graticuleStrokeWidth: this.radar?.graticuleStrokeWidth || 1, + graticuleOpacity: this.radar?.graticuleOpacity || 0.3, + legendBoxSize: this.radar?.legendBoxSize || 12, + legendFontSize: this.radar?.legendFontSize || 12, + }; + /* architecture */ this.archEdgeColor = this.archEdgeColor || '#777'; this.archEdgeArrowColor = this.archEdgeArrowColor || '#777'; diff --git a/packages/mermaid/src/themes/theme-dark.js b/packages/mermaid/src/themes/theme-dark.js index a0df8d4f3..c452eea9f 100644 --- a/packages/mermaid/src/themes/theme-dark.js +++ b/packages/mermaid/src/themes/theme-dark.js @@ -291,6 +291,20 @@ class Theme { blockFillColor: this.background, }; + /* radar */ + this.radar = { + axisColor: this.radar?.axisColor || this.lineColor, + axisStrokeWidth: this.radar?.axisStrokeWidth || 2, + axisLabelFontSize: this.radar?.axisLabelFontSize || 12, + curveOpacity: this.radar?.curveOpacity || 0.5, + curveStrokeWidth: this.radar?.curveStrokeWidth || 2, + graticuleColor: this.radar?.graticuleColor || '#DEDEDE', + graticuleStrokeWidth: this.radar?.graticuleStrokeWidth || 1, + graticuleOpacity: this.radar?.graticuleOpacity || 0.3, + legendBoxSize: this.radar?.legendBoxSize || 12, + legendFontSize: this.radar?.legendFontSize || 12, + }; + /* class */ this.classText = this.primaryTextColor; diff --git a/packages/mermaid/src/themes/theme-default.js b/packages/mermaid/src/themes/theme-default.js index 78f20a475..eba3ff101 100644 --- a/packages/mermaid/src/themes/theme-default.js +++ b/packages/mermaid/src/themes/theme-default.js @@ -291,6 +291,20 @@ class Theme { this.quadrantExternalBorderStrokeFill || this.primaryBorderColor; this.quadrantTitleFill = this.quadrantTitleFill || this.primaryTextColor; + /* radar */ + this.radar = { + axisColor: this.radar?.axisColor || this.lineColor, + axisStrokeWidth: this.radar?.axisStrokeWidth || 2, + axisLabelFontSize: this.radar?.axisLabelFontSize || 12, + curveOpacity: this.radar?.curveOpacity || 0.5, + curveStrokeWidth: this.radar?.curveStrokeWidth || 2, + graticuleColor: this.radar?.graticuleColor || '#DEDEDE', + graticuleStrokeWidth: this.radar?.graticuleStrokeWidth || 1, + graticuleOpacity: this.radar?.graticuleOpacity || 0.3, + legendBoxSize: this.radar?.legendBoxSize || 12, + legendFontSize: this.radar?.legendFontSize || 12, + }; + /* xychart */ this.xyChart = { backgroundColor: this.xyChart?.backgroundColor || this.background, diff --git a/packages/mermaid/src/themes/theme-forest.js b/packages/mermaid/src/themes/theme-forest.js index 34d965201..853b4d032 100644 --- a/packages/mermaid/src/themes/theme-forest.js +++ b/packages/mermaid/src/themes/theme-forest.js @@ -265,6 +265,20 @@ class Theme { blockFillColor: this.mainBkg, }; + /* radar */ + this.radar = { + axisColor: this.radar?.axisColor || this.lineColor, + axisStrokeWidth: this.radar?.axisStrokeWidth || 2, + axisLabelFontSize: this.radar?.axisLabelFontSize || 12, + curveOpacity: this.radar?.curveOpacity || 0.5, + curveStrokeWidth: this.radar?.curveStrokeWidth || 2, + graticuleColor: this.radar?.graticuleColor || '#DEDEDE', + graticuleStrokeWidth: this.radar?.graticuleStrokeWidth || 1, + graticuleOpacity: this.radar?.graticuleOpacity || 0.3, + legendBoxSize: this.radar?.legendBoxSize || 12, + legendFontSize: this.radar?.legendFontSize || 12, + }; + /* xychart */ this.xyChart = { backgroundColor: this.xyChart?.backgroundColor || this.background, diff --git a/packages/mermaid/src/themes/theme-neutral.js b/packages/mermaid/src/themes/theme-neutral.js index 8b7f34ed8..633a26849 100644 --- a/packages/mermaid/src/themes/theme-neutral.js +++ b/packages/mermaid/src/themes/theme-neutral.js @@ -303,6 +303,20 @@ class Theme { '#EEE,#6BB8E4,#8ACB88,#C7ACD6,#E8DCC2,#FFB2A8,#FFF380,#7E8D91,#FFD8B1,#FAF3E0', }; + /* radar */ + this.radar = { + axisColor: this.radar?.axisColor || this.lineColor, + axisStrokeWidth: this.radar?.axisStrokeWidth || 2, + axisLabelFontSize: this.radar?.axisLabelFontSize || 12, + curveOpacity: this.radar?.curveOpacity || 0.5, + curveStrokeWidth: this.radar?.curveStrokeWidth || 2, + graticuleColor: this.radar?.graticuleColor || '#DEDEDE', + graticuleStrokeWidth: this.radar?.graticuleStrokeWidth || 1, + graticuleOpacity: this.radar?.graticuleOpacity || 0.3, + legendBoxSize: this.radar?.legendBoxSize || 12, + legendFontSize: this.radar?.legendFontSize || 12, + }; + /* requirement-diagram */ this.requirementBackground = this.requirementBackground || this.primaryColor; this.requirementBorderColor = this.requirementBorderColor || this.primaryBorderColor; From 5e97b2f764e94d3268327d52f77f5d0b26a5992f Mon Sep 17 00:00:00 2001 From: Thomas Di Cizerone Date: Sun, 16 Mar 2025 18:17:07 +0100 Subject: [PATCH 095/177] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20Add=20Cypress=20te?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress/integration/rendering/radar.spec.js | 79 +++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 cypress/integration/rendering/radar.spec.js diff --git a/cypress/integration/rendering/radar.spec.js b/cypress/integration/rendering/radar.spec.js new file mode 100644 index 000000000..b0bc3f6e0 --- /dev/null +++ b/cypress/integration/rendering/radar.spec.js @@ -0,0 +1,79 @@ +import { imgSnapshotTest } from '../../helpers/util'; + +describe('radar structure', () => { + it('should render a simple radar diagram', () => { + imgSnapshotTest( + `radar-beta + title Best Radar Ever + axis A, B, C + curve c1{1, 2, 3} + ` + ); + }); + + it('should render a radar diagram with multiple curves', () => { + imgSnapshotTest( + `radar-beta + title Best Radar Ever + axis A, B, C + curve c1{1, 2, 3} + curve c2{2, 3, 1} + ` + ); + }); + + it('should render a complex radar diagram', () => { + imgSnapshotTest( + `radar-beta + title My favorite ninjas + axis Agility, Speed, Strength + axis Stam["Stamina"] , Intel["Intelligence"] + + curve Ninja1["Naruto Uzumaki"]{ + Agility 2, Speed 2, + Strength 3, Stam 5, + Intel 0 + } + curve Ninja2["Sasuke"]{2, 3, 4, 1, 5} + curve Ninja3 {3, 2, 1, 5, 4} + + showLegend true + ticks 3 + max 8 + min 0 + graticule polygon + ` + ); + cy.get('svg').should((svg) => { + expect(svg).to.have.length(1); + }); + }); + + it('should render radar diagram with config override', () => { + imgSnapshotTest( + `radar-beta + title Best Radar Ever + axis A,B,C + curve mycurve{1,2,3}`, + { radar: { marginTop: 100, axisScaleFactor: 0.5 } } + ); + }); + + it('should parse radar diagram with theme override', () => { + imgSnapshotTest( + `radar-beta + axis A,B,C + curve mycurve{1,2,3}`, + { theme: 'base', themeVariables: { fontSize: 80, cScale0: '#FF0000' } } + ); + }); + + it('should handle radar diagram with radar style override', () => { + imgSnapshotTest( + `radar-beta + axis A,B,C + curve mycurve{1,2,3}`, + { theme: 'base', themeVariables: { radar: { axisColor: '#FF0000' } } } + ); + }); +}); From a7f2c0bc344d0881c8486282bafd3a12856b481d Mon Sep 17 00:00:00 2001 From: Thomas Di Cizerone Date: Sun, 16 Mar 2025 18:18:18 +0100 Subject: [PATCH 096/177] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20Add=20Radar=20D?= =?UTF-8?q?emo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demos/index.html | 3 ++ demos/radar.html | 136 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 demos/radar.html diff --git a/demos/index.html b/demos/index.html index 07b51a313..d615bf347 100644 --- a/demos/index.html +++ b/demos/index.html @@ -91,6 +91,9 @@

  • Architecture

  • +
  • +

    Radar

    +
  • diff --git a/demos/radar.html b/demos/radar.html new file mode 100644 index 000000000..6e9f8df82 --- /dev/null +++ b/demos/radar.html @@ -0,0 +1,136 @@ + + + + + + Mermaid Quick Test Page + + + + + +

    Radar diagram demo

    + +
    +
    +      radar-beta 
    +        title My favorite ninjas
    +        axis Agility, Speed, Strength
    +        axis Stam["Stamina"] , Intel["Intelligence"]
    +      
    +        curve Ninja1["Naruto"]{
    +            Agility 2, Speed 2,
    +            Strength 3, Stam 5,
    +            Intel 0
    +        }
    +        curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
    +        curve Ninja3["Ninja"] {3, 2, 1, 5, 4}
    +      
    +        showLegend true
    +        ticks 3
    +        max 8
    +        min 0
    +        graticule circle
    +    
    + +
    +      ---
    +      config:
    +        radar:
    +          axisScaleFactor: 0.25
    +          axisLabelFactor: 0.95
    +      ---
    +      radar-beta 
    +        title DevOps Radar
    +        axis f["Feature Velocity"], s["Stability"]
    +        axis r["Resilience"], e["Efficiency"]
    +        axis c["Cost"], d["DevSecOps"]
    +      
    +        curve app1["App1"]{
    +          f 5, s 4.5, r 3.8, d 4.2, e 4.5, c 3.5
    +        }
    +        curve app2["App2"]{4, 3, 4, 3, 3, 4}, app3["App3"]{3, 2, 4, 3, 2, 3}
    +        curve app4["App4"]{2, 1, 3.2, 2.5, 1, 2}
    +      
    +        showLegend true
    +        ticks 3
    +        max 5
    +        graticule polygon
    +    
    + +
    +      %%{init: {'theme': 'forest'} }%%
    +      radar-beta 
    +        title Forest theme
    +        axis Agility, Speed, Strength
    +        axis Stam["Stamina"] , Intel["Intelligence"]
    +      
    +        curve Ninja1["Naruto"]{
    +            Agility 2, Speed 2,
    +            Strength 3, Stam 5,
    +            Intel 0
    +        }
    +        curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
    +        curve Ninja3["Ninja"] {3, 2, 1, 5, 4}
    +    
    + +
    +      %%{init: {'theme': 'dark'} }%%
    +      radar-beta 
    +        title Dark theme
    +        axis Agility, Speed, Strength
    +        axis Stam["Stamina"] , Intel["Intelligence"]
    +      
    +        curve Ninja1["Naruto"]{
    +            Agility 2, Speed 2,
    +            Strength 3, Stam 5,
    +            Intel 0
    +        }
    +        curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
    +        curve Ninja3["Ninja"] {3, 2, 1, 5, 4}
    +    
    +
    +      %%{init: {'theme': 'base', 'themeVariables': {'cScale0': '#ff0000', 'cScale1': '#00ff00', 'cScale2': '#0000ff'}} }%%
    +      radar-beta 
    +        title Custom colors
    +        axis Agility, Speed, Strength
    +        axis Stam["Stamina"] , Intel["Intelligence"]
    +
    +        curve Ninja1["Naruto"]{
    +            Agility 2, Speed 2,
    +            Strength 3, Stam 5,
    +            Intel 0
    +        }
    +        curve Ninja2["Sasuke"]{2, 3, 4, 1, 5}
    +        curve Ninja3["Ninja"] {3, 2, 1, 5, 4}
    +    
    +
    + + + + + From 12c120368d16f9b69d00fa44fb6bf20d887b60e1 Mon Sep 17 00:00:00 2001 From: Thomas Di Cizerone Date: Sun, 16 Mar 2025 18:19:09 +0100 Subject: [PATCH 097/177] =?UTF-8?q?=F0=9F=93=9A=20Add=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mermaid/src/docs/.vitepress/config.ts | 1 + .../src/docs/community/contributing.md | 16 ++ packages/mermaid/src/docs/syntax/radar.md | 198 ++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 packages/mermaid/src/docs/syntax/radar.md diff --git a/packages/mermaid/src/docs/.vitepress/config.ts b/packages/mermaid/src/docs/.vitepress/config.ts index f8f740177..d3f4a9aee 100644 --- a/packages/mermaid/src/docs/.vitepress/config.ts +++ b/packages/mermaid/src/docs/.vitepress/config.ts @@ -159,6 +159,7 @@ function sidebarSyntax() { { text: 'Packet 🔥', link: '/syntax/packet' }, { text: 'Kanban 🔥', link: '/syntax/kanban' }, { text: 'Architecture 🔥', link: '/syntax/architecture' }, + { text: 'Radar 🔥', link: '/syntax/radar' }, { text: 'Other Examples', link: '/syntax/examples' }, ], }, diff --git a/packages/mermaid/src/docs/community/contributing.md b/packages/mermaid/src/docs/community/contributing.md index 4cd649563..a962907df 100644 --- a/packages/mermaid/src/docs/community/contributing.md +++ b/packages/mermaid/src/docs/community/contributing.md @@ -240,6 +240,22 @@ Code is the heart of every software project. We strive to make it better. Who if The core of Mermaid is located under `packages/mermaid/src`. +### Building Mermaid Locally + +**Host** + +```bash +pnpm run build +``` + +**Docker** + +```bash +./run build +``` + +This will build the Mermaid library and the documentation site. + ### Running Mermaid Locally **Host** diff --git a/packages/mermaid/src/docs/syntax/radar.md b/packages/mermaid/src/docs/syntax/radar.md new file mode 100644 index 000000000..50293770c --- /dev/null +++ b/packages/mermaid/src/docs/syntax/radar.md @@ -0,0 +1,198 @@ +# Radar Diagram (v+) + +## Introduction + +A radar diagram is a simple way to plot low-dimensional data in a circular format. + +It is also known as a **radar chart**, **spider chart**, **star chart**, **cobweb chart**, **polar chart**, or **Kiviat diagram**. + +## Usage + +This diagram type is particularly useful for developers, data scientists, and engineers who require a clear and concise way to represent data in a circular format. + +It is commonly used to graphically summarize and compare the performance of multiple entities across multiple dimensions. + +## Syntax + +```md +radar-beta +axis A, B, C, D, E +curve c1{1,2,3,4,5} +curve c2{5,4,3,2,1} +... More Fields ... +``` + +## Examples + +```mermaid-example +--- +title: "Grades" +--- +radar-beta + axis m["Math"], s["Science"], e["English"] + axis h["History"], g["Geography"], a["Art"] + curve a["Alice"]{85, 90, 80, 70, 75, 90} + curve b["Bob"]{70, 75, 85, 80, 90, 85} + + max 100 + min 0 +``` + +```mermaid-example +radar-beta + title Restaurant Comparison + axis food["Food Quality"], service["Service"], price["Price"] + axis ambiance["Ambiance"], + + curve a["Restaurant A"]{4, 3, 2, 4} + curve b["Restaurant B"]{3, 4, 3, 3} + curve c["Restaurant C"]{2, 3, 4, 2} + curve d["Restaurant D"]{2, 2, 4, 3} + + graticule polygon + max 5 + +``` + +## Details of Syntax + +### Title + +`title`: The title is an optional field that allows to render a title at the top of the radar diagram. + +``` +radar-beta + title Title of the Radar Diagram + ... +``` + +### Axis + +`axis`: The axis keyword is used to define the axes of the radar diagram. + +Each axis is represented by an ID and an optional label. + +Multiple axes can be defined in a single line. + +``` +radar-beta + axis id1["Label1"] + axis id2["Label2"], id3["Label3"] + ... +``` + +### Curve + +`curve`: The curve keyword is used to define the data points for a curve in the radar diagram. + +Each curve is represented by an ID, an optional label, and a list of values. + +Values can be defined by a list of numbers or a list of key-value pairs. If key-value pairs are used, the key represents the axis ID and the value represents the data point. Else, the data points are assumed to be in the order of the axes defined. + +Multiple curves can be defined in a single line. + +``` +radar-beta + axis axis1, axis2, axis3 + curve id1["Label1"]{1, 2, 3} + curve id2["Label2"]{4, 5, 6}, id3{7, 8, 9} + curve id4{ axis3: 30, axis1: 20, axis2: 10 } + ... +``` + +### Options + +- `showLegend`: The showLegend keyword is used to show or hide the legend in the radar diagram. The legend is shown by default. +- `max`: The maximum value for the radar diagram. This is used to scale the radar diagram. If not provided, the maximum value is calculated from the data points. +- `min`: The minimum value for the radar diagram. This is used to scale the radar diagram. If not provided, the minimum value is `0`. +- `graticule`: The graticule keyword is used to define the type of graticule to be rendered in the radar diagram. The graticule can be `circle` or `polygon`. If not provided, the default graticule is `circle`. +- `ticks`: The ticks keyword is used to define the number of ticks on the graticule. It is the number of concentric circles or polygons drawn to indicate the scale of the radar diagram. If not provided, the default number of ticks is `5`. + +``` +radar-beta + ... + showLegend true + max 100 + min 0 + graticule circle + ticks 5 + ... +``` + +## Configuration + +Please refer to the [configuration](/config/schema-docs/config-defs-radar-diagram-config.html) guide for details. + +| Parameter | Description | Default Value | +| --------------- | ---------------------------------------- | ------------- | +| width | Width of the radar diagram | `600` | +| height | Height of the radar diagram | `600` | +| marginTop | Top margin of the radar diagram | `50` | +| marginBottom | Bottom margin of the radar diagram | `50` | +| marginLeft | Left margin of the radar diagram | `50` | +| marginRight | Right margin of the radar diagram | `50` | +| axisScaleFactor | Scale factor for the axis | `1` | +| axisLabelFactor | Factor to adjust the axis label position | `1.05` | +| curveTension | Tension for the rounded curves | `0.17` | + +## Theme Variables + +### Global Theme Variables + +```note +The default values for these variables depend on the theme used. To override the default values, set the desired values in the themeVariables section of the configuration: +%%{init: {"themeVariables": {"cScale0": "#FF0000", "cScale1": "#00FF00"}} }%% +``` + +Radar charts support the color scales `cScale${i}` where `i` is a number from `0` to the theme's maximum number of colors in its color scale. Usually, the maximum number of colors is `12`. + +| Property | Description | +| ---------- | ------------------------------ | +| fontSize | Font size of the title | +| titleColor | Color of the title | +| cScale${i} | Color scale for the i-th curve | + +### Radar Style Options + +```note +Specific variables for radar resides inside the `radar` key. To set the radar style options, use this syntax. +%%{init: {"themeVariables": {"radar": {"axisColor": "#FF0000"}} } }%% +``` + +| Property | Description | Default Value | +| -------------------- | ---------------------------- | ------------- | +| axisColor | Color of the axis lines | `black` | +| axisStrokeWidth | Width of the axis lines | `1` | +| axisLabelFontSize | Font size of the axis labels | `12px` | +| curveOpacity | Opacity of the curves | `0.7` | +| curveStrokeWidth | Width of the curves | `2` | +| graticuleColor | Color of the graticule | `black` | +| graticuleOpacity | Opacity of the graticule | `0.5` | +| graticuleStrokeWidth | Width of the graticule | `1` | +| legendBoxSize | Size of the legend box | `10` | +| legendFontSize | Font size of the legend | `14px` | + +## Example on config and theme + +```mermaid-example +--- +config: + radar: + axisScaleFactor: 0.25 + curveTension: 0.1 + theme: base + themeVariables: + cScale0: "#FF0000" + cScale1: "#00FF00" + cScale2: "#0000FF" + radar: + curveOpacity: 0 +--- +radar-beta + axis A, B, C, D, E + curve c1{1,2,3,4,5} + curve c2{5,4,3,2,1} + curve c3{3,3,3,3,3} +``` + + From 0f5125b5e30d1a708fa18c91b50cd14d25428289 Mon Sep 17 00:00:00 2001 From: Thomas Di Cizerone Date: Sun, 16 Mar 2025 18:34:00 +0100 Subject: [PATCH 098/177] =?UTF-8?q?=F0=9F=96=BC=EF=B8=8F=20Add=20Radar=20D?= =?UTF-8?q?emo=20with=20custom=20opacity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demos/radar.html | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/demos/radar.html b/demos/radar.html index 6e9f8df82..16d03abfe 100644 --- a/demos/radar.html +++ b/demos/radar.html @@ -113,6 +113,27 @@ curve Ninja3["Ninja"] {3, 2, 1, 5, 4} +
    +      ---
    +      config:
    +        radar:
    +          axisScaleFactor: 0.25
    +          curveTension: 0.1
    +        theme: base
    +        themeVariables:
    +          cScale0: "#FF0000"
    +          cScale1: "#00FF00"
    +          cScale2: "#0000FF"
    +          radar:
    +            curveOpacity: 0
    +      ---
    +      radar-beta
    +        title Custom colors, axisScaleFactor, curveTension, opacity
    +        axis A, B, C, D, E
    +        curve c1{1,2,3,4,5}
    +        curve c2{5,4,3,2,1}
    +        curve c3{3,3,3,3,3}
    +