Merge branch 'develop' into feature/4706_allow_notes_in_namespace

This commit is contained in:
kairi
2024-12-01 09:20:39 +09:00
committed by GitHub
25 changed files with 620 additions and 124 deletions

View File

@@ -1,5 +1,20 @@
# @mermaid-js/layout-elk
## 0.1.7
### Patch Changes
- [#6090](https://github.com/mermaid-js/mermaid/pull/6090) [`654097c`](https://github.com/mermaid-js/mermaid/commit/654097c43801b2d606bc3d2bef8c6fbc3301e9e4) Thanks [@knsv](https://github.com/knsv)! - fix: Updated offset calculations for diamond shape when handling intersections
## 0.1.6
### Patch Changes
- [#6081](https://github.com/mermaid-js/mermaid/pull/6081) [`68f41f6`](https://github.com/mermaid-js/mermaid/commit/68f41f685d2afe7d12f63aabf3de0c3461898471) Thanks [@knsv](https://github.com/knsv)! - fix: Elk rendering of Diamond shape intersections
- Updated dependencies [[`01b5079`](https://github.com/mermaid-js/mermaid/commit/01b5079562ec8d34ce9964910f168873843c68f8), [`1388662`](https://github.com/mermaid-js/mermaid/commit/1388662132cc829f9820c2e9970ae04e2dd90588), [`fe3cffb`](https://github.com/mermaid-js/mermaid/commit/fe3cffbb673a25b81989aacb06e5d0eda35326db)]:
- mermaid@11.4.1
## 0.1.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@mermaid-js/layout-elk",
"version": "0.1.5",
"version": "0.1.7",
"description": "ELK layout engine for mermaid",
"module": "dist/mermaid-layout-elk.core.mjs",
"types": "dist/layouts.d.ts",

View File

@@ -484,6 +484,8 @@ export const render = async (
const r3 = a1 * q1.x + b1 * q1.y + c1;
const r4 = a1 * q2.x + b1 * q2.y + c1;
const epsilon = 1e-6;
// Check signs of r3 and r4. If both point 3 and point 4 lie on
// same side of line 1, the line segments do not intersect.
if (r3 !== 0 && r4 !== 0 && sameSign(r3, r4)) {
@@ -502,7 +504,7 @@ export const render = async (
// Check signs of r1 and r2. If both point 1 and point 2 lie
// on same side of second line segment, the line segments do
// not intersect.
if (r1 !== 0 && r2 !== 0 && sameSign(r1, r2)) {
if (Math.abs(r1) < epsilon && Math.abs(r2) < epsilon && sameSign(r1, r2)) {
return /*DON'T_INTERSECT*/;
}
@@ -547,11 +549,11 @@ export const render = async (
{ x: x1 - w / 2, y: y1 },
];
log.debug(
`UIO diamondIntersection calc abc89:
`APA16 diamondIntersection calc abc89:
outsidePoint: ${JSON.stringify(outsidePoint)}
insidePoint : ${JSON.stringify(insidePoint)}
node : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`,
polyPoints
node-bounds : x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`,
JSON.stringify(polyPoints)
);
const intersections = [];
@@ -564,8 +566,8 @@ export const render = async (
minY = Math.min(minY, entry.y);
});
// const left = x1 - w / 2;
// const top = y1 + h / 2;
const left = x1 - w / 2 - minX;
const top = y1 - h / 2 - minY;
for (let i = 0; i < polyPoints.length; i++) {
const p1 = polyPoints[i];
@@ -573,8 +575,8 @@ export const render = async (
const intersect = intersectLine(
bounds,
outsidePoint,
{ x: p1.x, y: p1.y },
{ x: p2.x, y: p2.y }
{ x: left + p1.x, y: top + p1.y },
{ x: left + p2.x, y: top + p2.y }
);
if (intersect) {
@@ -703,14 +705,11 @@ export const render = async (
bounds: { x: any; y: any; width: any; height: any; padding: any },
isDiamond: boolean
) => {
log.debug('UIO cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
log.debug('APA18 cutPathAtIntersect Points:', _points, 'node:', bounds, 'isDiamond', isDiamond);
const points: any[] = [];
let lastPointOutside = _points[0];
let isInside = false;
_points.forEach((point: any) => {
// const node = clusterDb[edge.toCluster].node;
log.debug(' checking point', point, bounds);
// check if point is inside the boundary rect
if (!outsideNode(bounds, point) && !isInside) {
// First point inside the rect found
@@ -753,7 +752,6 @@ export const render = async (
}
}
});
log.debug('returning points', points);
return points;
};
@@ -905,7 +903,7 @@ export const render = async (
const offset = calcOffset(sourceId, targetId, parentLookupDb);
log.debug(
'offset',
'APA18 offset',
offset,
sourceId,
' ==> ',
@@ -968,48 +966,41 @@ export const render = async (
startNode.innerHTML
);
}
if (startNode.shape === 'diamond') {
if (startNode.shape === 'diamond' || startNode.shape === 'diam') {
edge.points.unshift({
x: startNode.x + startNode.width / 2 + offset.x,
y: startNode.y + startNode.height / 2 + offset.y,
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
});
}
if (endNode.shape === 'diamond') {
const x = endNode.x + endNode.width / 2 + offset.x;
// Add a point at the center of the diamond
if (
Math.abs(edge.points[edge.points.length - 1].y - endNode.y - offset.y) > 0.001 ||
Math.abs(edge.points[edge.points.length - 1].x - x) > 0.001
) {
edge.points.push({
x: endNode.x + endNode.width / 2 + offset.x,
y: endNode.y + endNode.height / 2 + offset.y,
});
}
if (endNode.shape === 'diamond' || endNode.shape === 'diam') {
edge.points.push({
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
});
}
edge.points = cutPathAtIntersect(
edge.points.reverse(),
{
x: startNode.x + startNode.width / 2 + offset.x,
y: startNode.y + startNode.height / 2 + offset.y,
x: startNode.offset.posX + startNode.width / 2,
y: startNode.offset.posY + startNode.height / 2,
width: sw,
height: startNode.height,
padding: startNode.padding,
},
startNode.shape === 'diamond'
startNode.shape === 'diamond' || startNode.shape === 'diam'
).reverse();
edge.points = cutPathAtIntersect(
edge.points,
{
x: endNode.x + ew / 2 + endNode.offset.x,
y: endNode.y + endNode.height / 2 + endNode.offset.y,
x: endNode.offset.posX + endNode.width / 2,
y: endNode.offset.posY + endNode.height / 2,
width: ew,
height: endNode.height,
padding: endNode.padding,
},
endNode.shape === 'diamond'
endNode.shape === 'diamond' || endNode.shape === 'diam'
);
const paths = insertEdge(

View File

@@ -1,5 +1,15 @@
# mermaid
## 11.4.1
### Patch Changes
- [#6059](https://github.com/mermaid-js/mermaid/pull/6059) [`01b5079`](https://github.com/mermaid-js/mermaid/commit/01b5079562ec8d34ce9964910f168873843c68f8) Thanks [@knsv](https://github.com/knsv)! - fix: Kanban diagrams will not render when adding a number as ticket id or assigned for a task
- [#6038](https://github.com/mermaid-js/mermaid/pull/6038) [`1388662`](https://github.com/mermaid-js/mermaid/commit/1388662132cc829f9820c2e9970ae04e2dd90588) Thanks [@knsv](https://github.com/knsv)! - fix: Intersection calculations for tilted cylinder/DAS when using handdrawn look. Some random seeds could cause the calculations to break.
- [#6079](https://github.com/mermaid-js/mermaid/pull/6079) [`fe3cffb`](https://github.com/mermaid-js/mermaid/commit/fe3cffbb673a25b81989aacb06e5d0eda35326db) Thanks [@aloisklink](https://github.com/aloisklink)! - Bump dompurify to `^3.2.1`. This removes the need for `@types/dompurify`.
## 11.4.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "11.4.0",
"version": "11.4.1",
"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",
@@ -71,7 +71,6 @@
"@iconify/utils": "^2.1.32",
"@mermaid-js/parser": "workspace:^",
"@types/d3": "^7.4.3",
"@types/dompurify": "^3.0.5",
"cytoscape": "^3.29.2",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
@@ -79,7 +78,7 @@
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.11",
"dayjs": "^1.11.10",
"dompurify": "^3.0.11 <3.1.7",
"dompurify": "^3.2.1",
"katex": "^0.16.9",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",

View File

@@ -13,6 +13,7 @@ import {
setDiagramTitle,
} from '../common/commonDb.js';
import type {
ArchitectureAlignment,
ArchitectureDB,
ArchitectureDirectionPair,
ArchitectureDirectionPairMap,
@@ -25,6 +26,7 @@ import type {
ArchitectureState,
} from './architectureTypes.js';
import {
getArchitectureDirectionAlignment,
getArchitectureDirectionPair,
isArchitectureDirection,
isArchitectureJunction,
@@ -211,12 +213,18 @@ const addEdge = function ({
const getEdges = (): ArchitectureEdge[] => state.records.edges;
/**
* Returns the current diagram's adjacency list & spatial map.
* Returns the current diagram's adjacency list, spatial map, & group alignments.
* If they have not been created, run the algorithms to generate them.
* @returns
*/
const getDataStructures = () => {
if (state.records.dataStructures === undefined) {
// Tracks how groups are aligned with one another. Generated while creating the adj list
const groupAlignments: Record<
string,
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
> = {};
// Create an adjacency list of the diagram to perform BFS on
// Outer reduce applied on all services
// Inner reduce applied on the edges for a service
@@ -224,6 +232,19 @@ const getDataStructures = () => {
Record<string, ArchitectureDirectionPairMap>
>((prevOuter, [id, service]) => {
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
// track the direction groups connect to one another
const lhsGroupId = getNode(edge.lhsId)?.in;
const rhsGroupId = getNode(edge.rhsId)?.in;
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
if (alignment !== 'bend') {
groupAlignments[lhsGroupId] ??= {};
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
groupAlignments[rhsGroupId] ??= {};
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
}
}
if (edge.lhsId === id) {
// source is LHS
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
@@ -245,6 +266,7 @@ const getDataStructures = () => {
// Configuration for the initial pass of BFS
const firstId = Object.keys(adjList)[0];
const visited = { [firstId]: 1 };
// If a key is present in this object, it has not been visited
const notVisited = Object.keys(adjList).reduce(
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
{} as Record<string, number>
@@ -283,6 +305,7 @@ const getDataStructures = () => {
state.records.dataStructures = {
adjList,
spatialMaps,
groupAlignments,
};
}
return state.records.dataStructures;

View File

@@ -12,7 +12,9 @@ import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import { getConfigField } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js';
import type {
ArchitectureAlignment,
ArchitectureDataStructures,
ArchitectureGroupAlignments,
ArchitectureJunction,
ArchitectureSpatialMap,
EdgeSingular,
@@ -149,25 +151,91 @@ function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) {
});
}
function getAlignments(spatialMaps: ArchitectureSpatialMap[]): fcose.FcoseAlignmentConstraint {
function getAlignments(
db: ArchitectureDB,
spatialMaps: ArchitectureSpatialMap[],
groupAlignments: ArchitectureGroupAlignments
): fcose.FcoseAlignmentConstraint {
/**
* Flattens the alignment object so nodes in different groups will be in the same alignment array IFF their groups don't connect in a conflicting alignment
*
* i.e., two groups which connect horizontally should not have nodes with vertical alignments to one another
*
* See: #5952
*
* @param alignmentObj - alignment object with the outer key being the row/col # and the inner key being the group name mapped to the nodes on that axis in the group
* @param alignmentDir - alignment direction
* @returns flattened alignment object with an arbitrary key mapping to nodes in the same row/col
*/
const flattenAlignments = (
alignmentObj: Record<number, Record<string, string[]>>,
alignmentDir: ArchitectureAlignment
): Record<string, string[]> => {
return Object.entries(alignmentObj).reduce(
(prev, [dir, alignments]) => {
// prev is the mapping of x/y coordinate to an array of the nodes in that row/column
let cnt = 0;
const arr = Object.entries(alignments); // [group name, array of nodes within the group on axis dir]
if (arr.length === 1) {
// If only one group exists in the row/column, we don't need to do anything else
prev[dir] = arr[0][1];
return prev;
}
for (let i = 0; i < arr.length - 1; i++) {
for (let j = i + 1; j < arr.length; j++) {
const [aGroupId, aNodeIds] = arr[i];
const [bGroupId, bNodeIds] = arr[j];
const alignment = groupAlignments[aGroupId]?.[bGroupId]; // Get how the two groups are intended to align (undefined if they aren't)
if (alignment === alignmentDir) {
// If the intended alignment between the two groups is the same as the alignment we are parsing
prev[dir] ??= [];
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; // add the node ids of both groups to the axis array in prev
} else if (aGroupId === 'default' || bGroupId === 'default') {
// If either of the groups are in the default space (not in a group), use the same behavior as above
prev[dir] ??= [];
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds];
} else {
// Otherwise, the nodes in the two groups are not intended to align
const keyA = `${dir}-${cnt++}`;
prev[keyA] = aNodeIds;
const keyB = `${dir}-${cnt++}`;
prev[keyB] = bNodeIds;
}
}
}
return prev;
},
{} as Record<string, string[]>
);
};
const alignments = spatialMaps.map((spatialMap) => {
const horizontalAlignments: Record<number, string[]> = {};
const verticalAlignments: Record<number, string[]> = {};
const horizontalAlignments: Record<number, Record<string, string[]>> = {};
const verticalAlignments: Record<number, Record<string, string[]>> = {};
// Group service ids in an object with their x and y coordinate as the key
Object.entries(spatialMap).forEach(([id, [x, y]]) => {
if (!horizontalAlignments[y]) {
horizontalAlignments[y] = [];
}
if (!verticalAlignments[x]) {
verticalAlignments[x] = [];
}
horizontalAlignments[y].push(id);
verticalAlignments[x].push(id);
const nodeGroup = db.getNode(id)?.in ?? 'default';
horizontalAlignments[y] ??= {};
horizontalAlignments[y][nodeGroup] ??= [];
horizontalAlignments[y][nodeGroup].push(id);
verticalAlignments[x] ??= {};
verticalAlignments[x][nodeGroup] ??= [];
verticalAlignments[x][nodeGroup].push(id);
});
// Merge the values of each object into a list if the inner list has at least 2 elements
return {
horiz: Object.values(horizontalAlignments).filter((arr) => arr.length > 1),
vert: Object.values(verticalAlignments).filter((arr) => arr.length > 1),
horiz: Object.values(flattenAlignments(horizontalAlignments, 'horizontal')).filter(
(arr) => arr.length > 1
),
vert: Object.values(flattenAlignments(verticalAlignments, 'vertical')).filter(
(arr) => arr.length > 1
),
};
});
@@ -244,7 +312,8 @@ function layoutArchitecture(
junctions: ArchitectureJunction[],
groups: ArchitectureGroup[],
edges: ArchitectureEdge[],
{ spatialMaps }: ArchitectureDataStructures
db: ArchitectureDB,
{ spatialMaps, groupAlignments }: ArchitectureDataStructures
): Promise<cytoscape.Core> {
return new Promise((resolve) => {
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
@@ -318,9 +387,8 @@ function layoutArchitecture(
addServices(services, cy);
addJunctions(junctions, cy);
addEdges(edges, cy);
// Use the spatial map to create alignment arrays for fcose
const alignmentConstraint = getAlignments(spatialMaps);
const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments);
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
@@ -454,7 +522,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
await drawServices(db, servicesElem, services);
drawJunctions(db, servicesElem, junctions);
const cy = await layoutArchitecture(services, junctions, groups, edges, ds);
const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);
await drawEdges(edgesElem, cy);
await drawGroups(groupElem, cy);

View File

@@ -7,6 +7,8 @@ import type cytoscape from 'cytoscape';
| Architecture Diagram Types |
\*=======================================*/
export type ArchitectureAlignment = 'vertical' | 'horizontal' | 'bend';
export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;
@@ -170,6 +172,18 @@ export const getArchitectureDirectionXYFactors = function (
}
};
export const getArchitectureDirectionAlignment = function (
a: ArchitectureDirection,
b: ArchitectureDirection
): ArchitectureAlignment {
if (isArchitectureDirectionXY(a, b)) {
return 'bend';
} else if (isArchitectureDirectionX(a)) {
return 'horizontal';
}
return 'vertical';
};
export interface ArchitectureStyleOptions {
archEdgeColor: string;
archEdgeArrowColor: string;
@@ -249,9 +263,27 @@ export interface ArchitectureDB extends DiagramDB {
export type ArchitectureAdjacencyList = Record<string, ArchitectureDirectionPairMap>;
export type ArchitectureSpatialMap = Record<string, number[]>;
/**
* Maps the direction that groups connect from.
*
* **Outer key**: ID of group A
*
* **Inner key**: ID of group B
*
* **Value**: 'vertical' or 'horizontal'
*
* Note: tmp[groupA][groupB] == tmp[groupB][groupA]
*/
export type ArchitectureGroupAlignments = Record<
string,
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
>;
export interface ArchitectureDataStructures {
adjList: ArchitectureAdjacencyList;
spatialMaps: ArchitectureSpatialMap[];
groupAlignments: ArchitectureGroupAlignments;
}
export interface ArchitectureState extends Record<string, unknown> {

View File

@@ -32,14 +32,14 @@ const setupDompurifyHooksIfNotSetup = (() => {
function setupDompurifyHooks() {
const TEMPORARY_ATTRIBUTE = 'data-temp-href-target';
DOMPurify.addHook('beforeSanitizeAttributes', (node: Element) => {
if (node.tagName === 'A' && node.hasAttribute('target')) {
DOMPurify.addHook('beforeSanitizeAttributes', (node) => {
if (node instanceof Element && node.tagName === 'A' && node.hasAttribute('target')) {
node.setAttribute(TEMPORARY_ATTRIBUTE, node.getAttribute('target') ?? '');
}
});
DOMPurify.addHook('afterSanitizeAttributes', (node: Element) => {
if (node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node instanceof Element && node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
node.setAttribute('target', node.getAttribute(TEMPORARY_ATTRIBUTE) ?? '');
node.removeAttribute(TEMPORARY_ATTRIBUTE);
if (node.getAttribute('target') === '_blank') {
@@ -83,7 +83,6 @@ export const sanitizeText = (text: string, config: MermaidConfig): string => {
return text;
}
if (config.dompurifyConfig) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
text = DOMPurify.sanitize(sanitizeMore(text, config), config.dompurifyConfig).toString();
} else {
text = DOMPurify.sanitize(sanitizeMore(text, config), {

View File

@@ -180,8 +180,6 @@ Communication tools and platforms
- [=Diagram block](https://github.com/zag/podlite/tree/main/packages/podlite-diagrams)
- [Standard Notes](https://standardnotes.com/)
- [Mermaid Extension](https://github.com/nienow/sn-mermaid)
- [Sublime Text 3](https://sublimetext.com)
- [Mermaid Package](https://packagecontrol.io/packages/Mermaid)
- [VS Code](https://code.visualstudio.com/)
- [Mermaid Editor](https://marketplace.visualstudio.com/items?itemName=tomoyukim.vscode-mermaid-editor)
- [Mermaid Export](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.mermaid-export)

View File

@@ -390,7 +390,7 @@ mermaid.ganttConfig = {
sectionFontSize: 24, // Font size for sections
numberSectionStyles: 1, // The number of alternating section styles
axisFormat: '%d/%m', // Date/time format of the axis
tickInterval: '1 week', // Axis ticks
tickInterval: '1week', // Axis ticks
topAxis: true, // When this flag is set, date labels will be added to the top of the chart
displayMode: 'compact', // Turns compact mode on
weekday: 'sunday', // On which day a week-based interval should start

View File

@@ -64,7 +64,7 @@ todo[Todo]
## Configuration Options
You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams tacketBaseUrl. This can be set as in the the following example:
You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams `ticketBaseUrl`. This can be set as in the the following example:
```yaml
---

View File

@@ -455,6 +455,7 @@ const render = async function (
svgCode = DOMPurify.sanitize(svgCode, {
ADD_TAGS: DOMPURIFY_TAGS,
ADD_ATTR: DOMPURIFY_ATTR,
HTML_INTEGRATION_POINTS: { foreignobject: true },
});
}