mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-03 19:17:00 +01:00
Merge branch 'develop' into sidv/deprecateMermaidAPI
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @ts-ignore: JISON typing missing
|
||||
import parser from '../../mermaid/src/diagrams/flowchart/parser/flow.jison';
|
||||
import * as db from '../../mermaid/src/diagrams/flowchart/flowDb.js';
|
||||
import db from '../../mermaid/src/diagrams/flowchart/flowDb.js';
|
||||
import styles from '../../mermaid/src/diagrams/flowchart/styles.js';
|
||||
import renderer from './flowRenderer-elk.js';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { findCommonAncestor } from './render-utils.js';
|
||||
import { labelHelper } from '../../mermaid/src/dagre-wrapper/shapes/util.js';
|
||||
import { getConfig } from '../../mermaid/src/config.js';
|
||||
import { log } from '../../mermaid/src/logger.js';
|
||||
import utils from '../../mermaid/src/utils.js';
|
||||
import { setupGraphViewbox } from '../../mermaid/src/setupGraphViewbox.js';
|
||||
import common from '../../mermaid/src/diagrams/common/common.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js';
|
||||
@@ -655,6 +656,11 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb, i
|
||||
.attr('d', curve(points))
|
||||
.attr('class', 'path ' + edgeData.classes)
|
||||
.attr('fill', 'none');
|
||||
Object.entries(edgeData).forEach(([key, value]) => {
|
||||
if (key !== 'classes') {
|
||||
edgePath.attr(key, value);
|
||||
}
|
||||
});
|
||||
const edgeG = edgesEl.insert('g').attr('class', 'edgeLabel');
|
||||
const edgeWithLabel = select(edgeG.node().appendChild(edge.labelEl));
|
||||
const box = edgeWithLabel.node().firstChild.getBoundingClientRect();
|
||||
@@ -702,21 +708,15 @@ const insertChildren = (nodeArray, parentLookupDb) => {
|
||||
*/
|
||||
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
// Add temporary render element
|
||||
diagObj.db.clear();
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
nodeDb = {};
|
||||
portPos = {};
|
||||
diagObj.db.setGen('gen-2');
|
||||
// Parse the graph definition
|
||||
diagObj.parser.parse(text);
|
||||
|
||||
const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy');
|
||||
let graph = {
|
||||
id: 'root',
|
||||
layoutOptions: {
|
||||
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
||||
'org.eclipse.elk.padding': '[top=100, left=100, bottom=110, right=110]',
|
||||
'elk.layered.spacing.edgeNodeBetweenLayers': '30',
|
||||
'elk.layered.spacing.edgeNodeBetweenLayers': conf?.nodeSpacing ? `${conf.nodeSpacing}` : '30',
|
||||
// 'elk.layered.mergeEdges': 'true',
|
||||
'elk.direction': 'DOWN',
|
||||
// 'elk.ports.sameLayerEdges': true,
|
||||
@@ -744,7 +744,6 @@ export const draw = async function (text, id, _version, diagObj) {
|
||||
graph.layoutOptions['elk.direction'] = 'LEFT';
|
||||
break;
|
||||
}
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
|
||||
// Find the root dom node to ne used in rendering
|
||||
// Handle root and document for when rendering in sandbox mode
|
||||
@@ -759,7 +758,6 @@ export const draw = async function (text, id, _version, diagObj) {
|
||||
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
|
||||
|
||||
const svg = root.select(`[id="${id}"]`);
|
||||
|
||||
// Define the supported markers for the diagram
|
||||
const markers = ['point', 'circle', 'cross'];
|
||||
|
||||
@@ -841,6 +839,7 @@ export const draw = async function (text, id, _version, diagObj) {
|
||||
log.info('after layout', JSON.stringify(graph, null, 2));
|
||||
const g = await elk.layout(graph);
|
||||
drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0);
|
||||
utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle());
|
||||
log.info('after layout', g);
|
||||
g.edges?.map((edge) => {
|
||||
insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb, id);
|
||||
|
||||
@@ -124,7 +124,7 @@ export const setConfig = (conf: MermaidConfig): MermaidConfig => {
|
||||
* | --------- | ------------------------- | ----------- | ------------------------------ |
|
||||
* | getConfig | Obtains the currentConfig | Get Request | Any Values from current Config |
|
||||
*
|
||||
* **Notes**: Returns **any** the currentConfig
|
||||
* **Notes**: Avoid calling this function repeatedly. Instead, store the result in a variable and use it, and pass it down to function calls.
|
||||
*
|
||||
* @returns The currentConfig
|
||||
*/
|
||||
|
||||
@@ -159,6 +159,7 @@ export interface MermaidConfig {
|
||||
dompurifyConfig?: DOMPurifyConfiguration;
|
||||
wrap?: boolean;
|
||||
fontSize?: number;
|
||||
markdownAutoWrap?: boolean;
|
||||
/**
|
||||
* Suppresses inserting 'Syntax error' diagram in the DOM.
|
||||
* This is useful when you want to control how to handle syntax errors in your application.
|
||||
|
||||
@@ -30,7 +30,7 @@ const rect = (parent, node) => {
|
||||
// .appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
|
||||
const text =
|
||||
node.labelType === 'markdown'
|
||||
? createText(label, node.labelText, { style: node.labelStyle, useHtmlLabels })
|
||||
? createText(label, node.labelText, { style: node.labelStyle, useHtmlLabels }, siteConfig)
|
||||
: label.node().appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
|
||||
|
||||
// Get the size of the label
|
||||
|
||||
@@ -18,15 +18,21 @@ export const clear = () => {
|
||||
};
|
||||
|
||||
export const insertEdgeLabel = (elem, edge) => {
|
||||
const useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||
const config = getConfig();
|
||||
const useHtmlLabels = evaluate(config.flowchart.htmlLabels);
|
||||
// Create the actual text element
|
||||
const labelElement =
|
||||
edge.labelType === 'markdown'
|
||||
? createText(elem, edge.label, {
|
||||
style: edge.labelStyle,
|
||||
useHtmlLabels,
|
||||
addSvgBackground: true,
|
||||
})
|
||||
? createText(
|
||||
elem,
|
||||
edge.label,
|
||||
{
|
||||
style: edge.labelStyle,
|
||||
useHtmlLabels,
|
||||
addSvgBackground: true,
|
||||
},
|
||||
config
|
||||
)
|
||||
: createLabel(edge.label, edge.labelStyle);
|
||||
|
||||
// Create outer g, edgeLabel, this will be positioned after graph layout
|
||||
|
||||
@@ -54,6 +54,14 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
|
||||
if (node && node.clusterNode) {
|
||||
// const children = graph.children(v);
|
||||
log.info('Cluster identified', v, node.width, graph.node(v));
|
||||
// `node.graph.setGraph` applies the graph configurations such as nodeSpacing to subgraphs as without this the default values would be used
|
||||
// We override only the `ranksep` and `nodesep` configurations to allow for setting subgraph spacing while avoiding overriding other properties
|
||||
const { ranksep, nodesep } = graph.graph();
|
||||
node.graph.setGraph({
|
||||
...node.graph.graph(),
|
||||
ranksep,
|
||||
nodesep,
|
||||
});
|
||||
const o = await recursiveRender(
|
||||
nodes,
|
||||
node.graph,
|
||||
|
||||
@@ -6,8 +6,9 @@ import { evaluate, sanitizeText } from '../../diagrams/common/common.js';
|
||||
import { decodeEntities } from '../../utils.js';
|
||||
|
||||
export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
const config = getConfig();
|
||||
let classes;
|
||||
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig().flowchart.htmlLabels);
|
||||
const useHtmlLabels = node.useHtmlLabels || evaluate(config.flowchart.htmlLabels);
|
||||
if (!_classes) {
|
||||
classes = 'node default';
|
||||
} else {
|
||||
@@ -35,26 +36,26 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
let text;
|
||||
if (node.labelType === 'markdown') {
|
||||
// text = textNode;
|
||||
text = createText(label, sanitizeText(decodeEntities(labelText), getConfig()), {
|
||||
useHtmlLabels,
|
||||
width: node.width || getConfig().flowchart.wrappingWidth,
|
||||
classes: 'markdown-node-label',
|
||||
});
|
||||
text = createText(
|
||||
label,
|
||||
sanitizeText(decodeEntities(labelText), config),
|
||||
{
|
||||
useHtmlLabels,
|
||||
width: node.width || config.flowchart.wrappingWidth,
|
||||
classes: 'markdown-node-label',
|
||||
},
|
||||
config
|
||||
);
|
||||
} else {
|
||||
text = textNode.appendChild(
|
||||
createLabel(
|
||||
sanitizeText(decodeEntities(labelText), getConfig()),
|
||||
node.labelStyle,
|
||||
false,
|
||||
isNode
|
||||
)
|
||||
createLabel(sanitizeText(decodeEntities(labelText), config), node.labelStyle, false, isNode)
|
||||
);
|
||||
}
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
const halfPadding = node.padding / 2;
|
||||
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
if (evaluate(config.flowchart.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
|
||||
@@ -76,8 +77,8 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
|
||||
if (noImgText) {
|
||||
// default size if no text
|
||||
const bodyFontSize = getConfig().fontSize
|
||||
? getConfig().fontSize
|
||||
const bodyFontSize = config.fontSize
|
||||
? config.fontSize
|
||||
: window.getComputedStyle(document.body).fontSize;
|
||||
const enlargingFactor = 5;
|
||||
const width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px';
|
||||
|
||||
@@ -346,11 +346,9 @@ export const renderKatex = async (text: string, config: MermaidConfig): Promise<
|
||||
.split(lineBreakRegex)
|
||||
.map((line) =>
|
||||
hasKatex(line)
|
||||
? `
|
||||
<div style="display: flex; align-items: center; justify-content: center; white-space: nowrap;">
|
||||
? `<div style="display: flex; align-items: center; justify-content: center; white-space: nowrap;">
|
||||
${line}
|
||||
</div>
|
||||
`
|
||||
</div>`
|
||||
: `<div>${line}</div>`
|
||||
)
|
||||
.join('')
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle set direction', function () {
|
||||
it('should handle set direction top to bottom', function () {
|
||||
const str = 'gitGraph TB:\n' + 'commit\n';
|
||||
|
||||
parser.parse(str);
|
||||
@@ -66,6 +66,18 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle set direction bottom to top', function () {
|
||||
const str = 'gitGraph BT:\n' + 'commit\n';
|
||||
|
||||
parser.parse(str);
|
||||
const commits = parser.yy.getCommits();
|
||||
|
||||
expect(Object.keys(commits).length).toBe(1);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
expect(parser.yy.getDirection()).toBe('BT');
|
||||
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should checkout a branch', function () {
|
||||
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n';
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ let commitPos = {};
|
||||
let lanes = [];
|
||||
let maxPos = 0;
|
||||
let dir = 'LR';
|
||||
let defaultPos = 30;
|
||||
const clear = () => {
|
||||
branchPos = {};
|
||||
commitPos = {};
|
||||
@@ -77,7 +78,7 @@ const findClosestParent = (parents) => {
|
||||
let maxPosition = 0;
|
||||
|
||||
parents.forEach((parent) => {
|
||||
const parentPosition = dir === 'TB' ? commitPos[parent].y : commitPos[parent].x;
|
||||
const parentPosition = dir === 'TB' || dir === 'BT' ? commitPos[parent].y : commitPos[parent].x;
|
||||
if (parentPosition >= maxPosition) {
|
||||
closestParent = parent;
|
||||
maxPosition = parentPosition;
|
||||
@@ -87,6 +88,83 @@ const findClosestParent = (parents) => {
|
||||
return closestParent || undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for the closest parent from the parents list passed as argument for Bottom-to-Top orientation.
|
||||
* The parents list comes from an individual commit. The closest parent is actually
|
||||
* the one farther down the graph, since that means it is closer to its child.
|
||||
*
|
||||
* @param {string[]} parents
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
const findClosestParentBT = (parents) => {
|
||||
let closestParent = '';
|
||||
let maxPosition = Infinity;
|
||||
|
||||
parents.forEach((parent) => {
|
||||
const parentPosition = commitPos[parent].y;
|
||||
if (parentPosition <= maxPosition) {
|
||||
closestParent = parent;
|
||||
maxPosition = parentPosition;
|
||||
}
|
||||
});
|
||||
|
||||
return closestParent || undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the position of the commit elements when the orientation is set to BT-Parallel.
|
||||
* This is needed to render the chart in Bottom-to-Top mode while keeping the parallel
|
||||
* commits in the correct position. First, it finds the correct position of the root commit
|
||||
* using the findClosestParent method. Then, it uses the findClosestParentBT to set the position
|
||||
* of the remaining commits.
|
||||
*
|
||||
* @param {any} sortedKeys
|
||||
* @param {any} commits
|
||||
* @param {any} defaultPos
|
||||
* @param {any} commitStep
|
||||
* @param {any} layoutOffset
|
||||
*/
|
||||
const setParallelBTPos = (sortedKeys, commits, defaultPos, commitStep, layoutOffset) => {
|
||||
let curPos = defaultPos;
|
||||
let maxPosition = defaultPos;
|
||||
let roots = [];
|
||||
sortedKeys.forEach((key) => {
|
||||
const commit = commits[key];
|
||||
if (commit.parents.length) {
|
||||
const closestParent = findClosestParent(commit.parents);
|
||||
curPos = commitPos[closestParent].y + commitStep;
|
||||
if (curPos >= maxPosition) {
|
||||
maxPosition = curPos;
|
||||
}
|
||||
} else {
|
||||
roots.push(commit);
|
||||
}
|
||||
const x = branchPos[commit.branch].pos;
|
||||
const y = curPos + layoutOffset;
|
||||
commitPos[commit.id] = { x: x, y: y };
|
||||
});
|
||||
curPos = maxPosition;
|
||||
roots.forEach((commit) => {
|
||||
const posWithOffset = curPos + defaultPos;
|
||||
const y = posWithOffset;
|
||||
const x = branchPos[commit.branch].pos;
|
||||
commitPos[commit.id] = { x: x, y: y };
|
||||
});
|
||||
sortedKeys.forEach((key) => {
|
||||
const commit = commits[key];
|
||||
if (commit.parents.length) {
|
||||
const closestParent = findClosestParentBT(commit.parents);
|
||||
curPos = commitPos[closestParent].y - commitStep;
|
||||
if (curPos <= maxPosition) {
|
||||
maxPosition = curPos;
|
||||
}
|
||||
const x = branchPos[commit.branch].pos;
|
||||
const y = curPos - layoutOffset;
|
||||
commitPos[commit.id] = { x: x, y: y };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws the commits with its symbol and labels. The function has two modes, one which only
|
||||
* calculates the positions and one that does the actual drawing. This for a simple way getting the
|
||||
@@ -102,39 +180,54 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
const gLabels = svg.append('g').attr('class', 'commit-labels');
|
||||
let pos = 0;
|
||||
|
||||
if (dir === 'TB') {
|
||||
pos = 30;
|
||||
if (dir === 'TB' || dir === 'BT') {
|
||||
pos = defaultPos;
|
||||
}
|
||||
|
||||
const keys = Object.keys(commits);
|
||||
const sortedKeys = keys.sort((a, b) => {
|
||||
return commits[a].seq - commits[b].seq;
|
||||
});
|
||||
|
||||
const isParallelCommits = gitGraphConfig.parallelCommits;
|
||||
const layoutOffset = 10;
|
||||
const commitStep = 40;
|
||||
let sortedKeys =
|
||||
dir !== 'BT' || (dir === 'BT' && isParallelCommits)
|
||||
? keys.sort((a, b) => {
|
||||
return commits[a].seq - commits[b].seq;
|
||||
})
|
||||
: keys
|
||||
.sort((a, b) => {
|
||||
return commits[a].seq - commits[b].seq;
|
||||
})
|
||||
.reverse();
|
||||
|
||||
if (dir === 'BT' && isParallelCommits) {
|
||||
setParallelBTPos(sortedKeys, commits, pos, commitStep, layoutOffset);
|
||||
sortedKeys = sortedKeys.reverse();
|
||||
}
|
||||
sortedKeys.forEach((key) => {
|
||||
const commit = commits[key];
|
||||
|
||||
if (isParallelCommits) {
|
||||
if (commit.parents.length) {
|
||||
const closestParent = findClosestParent(commit.parents);
|
||||
pos =
|
||||
dir === 'TB'
|
||||
? commitPos[closestParent].y + commitStep
|
||||
: commitPos[closestParent].x + commitStep;
|
||||
} else {
|
||||
pos = 0;
|
||||
const closestParent =
|
||||
dir === 'BT' ? findClosestParentBT(commit.parents) : findClosestParent(commit.parents);
|
||||
if (dir === 'TB') {
|
||||
pos = 30;
|
||||
pos = commitPos[closestParent].y + commitStep;
|
||||
} else if (dir === 'BT') {
|
||||
pos = commitPos[key].y - commitStep;
|
||||
} else {
|
||||
pos = commitPos[closestParent].x + commitStep;
|
||||
}
|
||||
} else {
|
||||
if (dir === 'TB') {
|
||||
pos = defaultPos;
|
||||
} else if (dir === 'BT') {
|
||||
pos = commitPos[key].y - commitStep;
|
||||
} else {
|
||||
pos = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const posWithOffset = pos + layoutOffset;
|
||||
const y = dir === 'TB' ? posWithOffset : branchPos[commit.branch].pos;
|
||||
const x = dir === 'TB' ? branchPos[commit.branch].pos : posWithOffset;
|
||||
const posWithOffset = dir === 'BT' && isParallelCommits ? pos : pos + layoutOffset;
|
||||
const y = dir === 'TB' || dir === 'BT' ? posWithOffset : branchPos[commit.branch].pos;
|
||||
const x = dir === 'TB' || dir === 'BT' ? branchPos[commit.branch].pos : posWithOffset;
|
||||
|
||||
// Don't draw the commits now but calculate the positioning which is used by the branch lines etc.
|
||||
if (modifyGraph) {
|
||||
@@ -258,7 +351,7 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dir === 'TB') {
|
||||
if (dir === 'TB' || dir === 'BT') {
|
||||
commitPos[commit.id] = { x: x, y: posWithOffset };
|
||||
} else {
|
||||
commitPos[commit.id] = { x: posWithOffset, y: y };
|
||||
@@ -295,16 +388,14 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
.attr('width', bbox.width + 2 * py)
|
||||
.attr('height', bbox.height + 2 * py);
|
||||
|
||||
if (dir === 'TB') {
|
||||
if (dir === 'TB' || dir === 'BT') {
|
||||
labelBkg.attr('x', x - (bbox.width + 4 * px + 5)).attr('y', y - 12);
|
||||
text.attr('x', x - (bbox.width + 4 * px)).attr('y', y + bbox.height - 12);
|
||||
}
|
||||
|
||||
if (dir !== 'TB') {
|
||||
} else {
|
||||
text.attr('x', posWithOffset - bbox.width / 2);
|
||||
}
|
||||
if (gitGraphConfig.rotateCommitLabel) {
|
||||
if (dir === 'TB') {
|
||||
if (dir === 'TB' || dir === 'BT') {
|
||||
text.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')');
|
||||
labelBkg.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')');
|
||||
} else {
|
||||
@@ -348,7 +439,7 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
.attr('r', 1.5)
|
||||
.attr('class', 'tag-hole');
|
||||
|
||||
if (dir === 'TB') {
|
||||
if (dir === 'TB' || dir === 'BT') {
|
||||
rect
|
||||
.attr('class', 'tag-label-bkg')
|
||||
.attr(
|
||||
@@ -373,7 +464,7 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
pos += commitStep + layoutOffset;
|
||||
pos = dir === 'BT' && isParallelCommits ? pos + commitStep : pos + commitStep + layoutOffset;
|
||||
if (pos > maxPos) {
|
||||
maxPos = pos;
|
||||
}
|
||||
@@ -389,7 +480,6 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
*
|
||||
* @param {any} commitA
|
||||
* @param {any} commitB
|
||||
* @param branchToGetCurve
|
||||
* @param p1
|
||||
* @param p2
|
||||
* @param allCommits
|
||||
@@ -402,7 +492,7 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
* return true
|
||||
*/
|
||||
const shouldRerouteArrow = (commitA, commitB, p1, p2, allCommits) => {
|
||||
const commitBIsFurthest = dir === 'TB' ? p1.x < p2.x : p1.y < p2.y;
|
||||
const commitBIsFurthest = dir === 'TB' || dir === 'BT' ? p1.x < p2.x : p1.y < p2.y;
|
||||
const branchToGetCurve = commitBIsFurthest ? commitB.branch : commitA.branch;
|
||||
const isOnBranchToGetCurve = (x) => x.branch === branchToGetCurve;
|
||||
const isBetweenCommits = (x) => x.seq > commitA.seq && x.seq < commitB.seq;
|
||||
@@ -485,6 +575,21 @@ const drawArrow = (svg, commitA, commitB, allCommits) => {
|
||||
p1.y + offset
|
||||
} L ${lineX} ${p2.y - radius} ${arc2} ${lineX - offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
} else if (dir === 'BT') {
|
||||
if (p1.x < p2.x) {
|
||||
// Source commit is on branch position left of destination commit
|
||||
// so render arrow rightward with colour of destination branch
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${lineX - radius} ${p1.y} ${arc} ${lineX} ${
|
||||
p1.y - offset
|
||||
} L ${lineX} ${p2.y + radius} ${arc2} ${lineX + offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
// Source commit is on branch position right of destination commit
|
||||
// so render arrow leftward with colour of source branch
|
||||
colorClassNum = branchPos[commitA.branch].index;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${lineX + radius} ${p1.y} ${arc2} ${lineX} ${
|
||||
p1.y - offset
|
||||
} L ${lineX} ${p2.y + radius} ${arc} ${lineX - offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
} else {
|
||||
if (p1.y < p2.y) {
|
||||
// Source commit is on branch positioned above destination commit
|
||||
@@ -524,7 +629,6 @@ const drawArrow = (svg, commitA, commitB, allCommits) => {
|
||||
arc2 = 'A 20 20, 0, 0, 1,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
|
||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${
|
||||
p2.y
|
||||
@@ -536,6 +640,38 @@ const drawArrow = (svg, commitA, commitB, allCommits) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (p1.x === p2.x) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
} else if (dir === 'BT') {
|
||||
if (p1.x < p2.x) {
|
||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc2} ${p1.x + offset} ${
|
||||
p2.y
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${
|
||||
p1.y - offset
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
}
|
||||
if (p1.x > p2.x) {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
arc2 = 'A 20 20, 0, 0, 1,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
|
||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y + radius} ${arc} ${p1.x - offset} ${
|
||||
p2.y
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${
|
||||
p1.y - offset
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (p1.x === p2.x) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
@@ -607,12 +743,16 @@ const drawBranches = (svg, branches) => {
|
||||
line.attr('class', 'branch branch' + adjustIndexForTheme);
|
||||
|
||||
if (dir === 'TB') {
|
||||
line.attr('y1', 30);
|
||||
line.attr('y1', defaultPos);
|
||||
line.attr('x1', pos);
|
||||
line.attr('y2', maxPos);
|
||||
line.attr('x2', pos);
|
||||
} else if (dir === 'BT') {
|
||||
line.attr('y1', maxPos);
|
||||
line.attr('x1', pos);
|
||||
line.attr('y2', defaultPos);
|
||||
line.attr('x2', pos);
|
||||
}
|
||||
|
||||
lanes.push(pos);
|
||||
|
||||
let name = branch.name;
|
||||
@@ -646,8 +786,10 @@ const drawBranches = (svg, branches) => {
|
||||
if (dir === 'TB') {
|
||||
bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', 0);
|
||||
label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + 0 + ')');
|
||||
}
|
||||
if (dir !== 'TB') {
|
||||
} else if (dir === 'BT') {
|
||||
bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', maxPos);
|
||||
label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + maxPos + ')');
|
||||
} else {
|
||||
bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')');
|
||||
}
|
||||
});
|
||||
@@ -681,7 +823,10 @@ export const draw = function (txt, id, ver, diagObj) {
|
||||
let bbox = labelElement.getBBox();
|
||||
|
||||
branchPos[branch.name] = { pos, index };
|
||||
pos += 50 + (gitGraphConfig.rotateCommitLabel ? 40 : 0) + (dir === 'TB' ? bbox.width / 2 : 0);
|
||||
pos +=
|
||||
50 +
|
||||
(gitGraphConfig.rotateCommitLabel ? 40 : 0) +
|
||||
(dir === 'TB' || dir === 'BT' ? bbox.width / 2 : 0);
|
||||
label.remove();
|
||||
branchLabel.remove();
|
||||
g.remove();
|
||||
|
||||
@@ -44,6 +44,7 @@ cherry\-pick(?=\s|$) return 'CHERRY_PICK';
|
||||
checkout(?=\s|$) return 'CHECKOUT';
|
||||
"LR" return 'DIR';
|
||||
"TB" return 'DIR';
|
||||
"BT" return 'DIR';
|
||||
":" return ':';
|
||||
"^" return 'CARET'
|
||||
"options"\r?\n this.begin("options"); //
|
||||
|
||||
@@ -196,11 +196,16 @@ export const drawNode = function (
|
||||
// Create the wrapped text element
|
||||
const textElem = nodeElem.append('g');
|
||||
const description = node.descr.replace(/(<br\/*>)/g, '\n');
|
||||
const newEl = createText(textElem, description, {
|
||||
useHtmlLabels: htmlLabels,
|
||||
width: node.width,
|
||||
classes: 'mindmap-node-label',
|
||||
});
|
||||
const newEl = createText(
|
||||
textElem,
|
||||
description,
|
||||
{
|
||||
useHtmlLabels: htmlLabels,
|
||||
width: node.width,
|
||||
classes: 'mindmap-node-label',
|
||||
},
|
||||
conf
|
||||
);
|
||||
|
||||
if (!htmlLabels) {
|
||||
textElem
|
||||
|
||||
@@ -537,6 +537,16 @@ Formatting:
|
||||
|
||||
This feature is applicable to node labels, edge labels, and subgraph labels.
|
||||
|
||||
The auto wrapping can be disabled by using
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
markdownAutoWrap: false
|
||||
---
|
||||
graph LR
|
||||
```
|
||||
|
||||
## Interaction
|
||||
|
||||
It is possible to bind a click event to a node, the click can lead to either a javascript callback or to a link which will be opened in a new browser tab.
|
||||
|
||||
@@ -519,9 +519,9 @@ Here, we have changed the default main branch name to `MetroLine1`.
|
||||
|
||||
## Orientation (v10.3.0+)
|
||||
|
||||
Mermaid supports two graph orientations: **Left-to-Right** (default) and **Top-to-Bottom**.
|
||||
Mermaid supports three graph orientations: **Left-to-Right** (default), **Top-to-Bottom**, and **Bottom-to-Top**.
|
||||
|
||||
You can set this with either `LR:` (for [**Left-to-Right**](#left-to-right-default-lr)) or `TB:` (for [**Top-to-Bottom**](#top-to-bottom-tb)) after `gitGraph`.
|
||||
You can set this with either `LR:` (for [**Left-to-Right**](#left-to-right-default-lr)), `TB:` (for [**Top-to-Bottom**](#top-to-bottom-tb)) or `BT:` (for [**Bottom-to-Top**](#bottom-to-top-bt)) after `gitGraph`.
|
||||
|
||||
### Left to Right (default, `LR:`)
|
||||
|
||||
@@ -569,6 +569,29 @@ Usage example:
|
||||
commit
|
||||
```
|
||||
|
||||
### Bottom to Top (`BT:`) (v<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
In `BT` (**Bottom-to-Top**) orientation, the commits run from bottom to top of the graph and branches are arranged side-by-side.
|
||||
|
||||
To orient the graph this way, you need to add `BT:` after gitGraph.
|
||||
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
gitGraph BT:
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
commit
|
||||
commit
|
||||
checkout main
|
||||
commit
|
||||
commit
|
||||
merge develop
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
## Parallel commits (v10.8.0+)
|
||||
|
||||
Commits in Mermaid display temporal information in gitgraph by default. For example if two commits are one commit away from its parent, the commit that was made earlier is rendered closer to its parent. You can turn this off by enabling the `parallelCommits` flag.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// @ts-nocheck TODO: Fix types
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
import type { Group } from '../diagram-api/types.js';
|
||||
import type { D3TSpanElement, D3TextElement } from '../diagrams/common/commonTypes.js';
|
||||
import { log } from '../logger.js';
|
||||
@@ -21,8 +22,7 @@ function addHtmlSpan(element, node, width, classes, addBackground = false) {
|
||||
const label = node.label;
|
||||
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
|
||||
div.html(
|
||||
`
|
||||
<span class="${labelClass} ${classes}" ` +
|
||||
`<span class="${labelClass} ${classes}" ` +
|
||||
(node.labelStyle ? 'style="' + node.labelStyle + '"' : '') +
|
||||
'>' +
|
||||
label +
|
||||
@@ -181,14 +181,14 @@ export const createText = (
|
||||
isNode = true,
|
||||
width = 200,
|
||||
addSvgBackground = false,
|
||||
} = {}
|
||||
} = {},
|
||||
config: MermaidConfig
|
||||
) => {
|
||||
log.info('createText', text, style, isTitle, classes, useHtmlLabels, isNode, addSvgBackground);
|
||||
if (useHtmlLabels) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
// text = text.replace(/\\n|\n/g, '<br />');
|
||||
const htmlText = markdownToHTML(text);
|
||||
// log.info('markdownToHTML' + text, markdownToHTML(text));
|
||||
|
||||
const htmlText = markdownToHTML(text, config);
|
||||
const node = {
|
||||
isNode,
|
||||
label: decodeEntities(htmlText).replace(
|
||||
@@ -200,7 +200,7 @@ export const createText = (
|
||||
const vertexNode = addHtmlSpan(el, node, width, classes, addSvgBackground);
|
||||
return vertexNode;
|
||||
} else {
|
||||
const structuredText = markdownToLines(text);
|
||||
const structuredText = markdownToLines(text, config);
|
||||
const svgLabel = createFormattedText(width, el, structuredText, addSvgBackground);
|
||||
return svgLabel;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
import { markdownToLines, markdownToHTML } from './handle-markdown-text.js';
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
@@ -203,6 +204,31 @@ Word!`;
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToLines - No auto wrapping', () => {
|
||||
expect(
|
||||
markdownToLines(
|
||||
`Hello, how do
|
||||
you do?`,
|
||||
{ markdownAutoWrap: false }
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
"content": "Hello, how do",
|
||||
"type": "normal",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"content": "you do?",
|
||||
"type": "normal",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('markdownToHTML - Basic test', () => {
|
||||
const input = `This is regular text
|
||||
Here is a new line
|
||||
@@ -262,3 +288,13 @@ test('markdownToHTML - Unsupported formatting', () => {
|
||||
- l3`)
|
||||
).toMatchInlineSnapshot('"<p>Hello</p>Unsupported markdown: list"');
|
||||
});
|
||||
|
||||
test('markdownToHTML - no auto wrapping', () => {
|
||||
expect(
|
||||
markdownToHTML(
|
||||
`Hello, how do
|
||||
you do?`,
|
||||
{ markdownAutoWrap: false }
|
||||
)
|
||||
).toMatchInlineSnapshot('"<p>Hello, how do<br/>you do?</p>"');
|
||||
});
|
||||
|
||||
@@ -2,24 +2,28 @@ import type { Content } from 'mdast';
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||
import { dedent } from 'ts-dedent';
|
||||
import type { MarkdownLine, MarkdownWordType } from './types.js';
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
|
||||
/**
|
||||
* @param markdown - markdown to process
|
||||
* @returns processed markdown
|
||||
*/
|
||||
function preprocessMarkdown(markdown: string): string {
|
||||
function preprocessMarkdown(markdown: string, { markdownAutoWrap }: MermaidConfig): string {
|
||||
// Replace multiple newlines with a single newline
|
||||
const withoutMultipleNewlines = markdown.replace(/\n{2,}/g, '\n');
|
||||
// Remove extra spaces at the beginning of each line
|
||||
const withoutExtraSpaces = dedent(withoutMultipleNewlines);
|
||||
if (markdownAutoWrap === false) {
|
||||
return withoutExtraSpaces.replace(/ /g, ' ');
|
||||
}
|
||||
return withoutExtraSpaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param markdown - markdown to split into lines
|
||||
*/
|
||||
export function markdownToLines(markdown: string): MarkdownLine[] {
|
||||
const preprocessedMarkdown = preprocessMarkdown(markdown);
|
||||
export function markdownToLines(markdown: string, config: MermaidConfig = {}): MarkdownLine[] {
|
||||
const preprocessedMarkdown = preprocessMarkdown(markdown, config);
|
||||
const { children } = fromMarkdown(preprocessedMarkdown);
|
||||
const lines: MarkdownLine[] = [[]];
|
||||
let currentLine = 0;
|
||||
@@ -56,11 +60,14 @@ export function markdownToLines(markdown: string): MarkdownLine[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function markdownToHTML(markdown: string) {
|
||||
export function markdownToHTML(markdown: string, { markdownAutoWrap }: MermaidConfig = {}) {
|
||||
const { children } = fromMarkdown(markdown);
|
||||
|
||||
function output(node: Content): string {
|
||||
if (node.type === 'text') {
|
||||
if (markdownAutoWrap === false) {
|
||||
return node.value.replace(/\n/g, '<br/>').replace(/ /g, ' ');
|
||||
}
|
||||
return node.value.replace(/\n/g, '<br/>');
|
||||
} else if (node.type === 'strong') {
|
||||
return `<strong>${node.children.map(output).join('')}</strong>`;
|
||||
|
||||
@@ -243,6 +243,9 @@ properties:
|
||||
fontSize:
|
||||
type: number
|
||||
default: 16
|
||||
markdownAutoWrap:
|
||||
type: boolean
|
||||
default: true
|
||||
suppressErrorRendering:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
Reference in New Issue
Block a user