Tidy-tree POC

This commit is contained in:
Knut Sveidqvist
2025-06-11 16:55:58 +02:00
parent e7208622f7
commit eadb343292
11 changed files with 1387 additions and 126 deletions

View File

@@ -105,6 +105,29 @@
</head>
<body>
<pre id="diagram4" class="mermaid">
---
config:
layout: tidy-tree
---
mindmap
root((mindmap))
Origins
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid">
---
config:
layout: elk
---
flowchart TB
A --> n0["1"]
A --> n1["2"]
A --> n2["3"]
A --> n3["4"]
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
@@ -127,10 +150,8 @@
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: cose-bilkent
@@ -138,23 +159,67 @@
mindmap
root((mindmap))
Origins
Europe
Asia
East
West
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
---
mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness&lt;br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid
</pre>
<pre id="diagram4" class="mermaid2">
---
config:
layout: cose-bilkent
---
flowchart LR
root{mindmap} --- Origins --- Europe
Origins --> Asia
root --- Background --- Rich
Background --- Poor
subgraph apa
Background
Rich
Poor
Poor
end
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: cose-bilkent
layout: elk
---
flowchart LR
root{mindmap} --- Origins --- Europe
@@ -269,7 +334,7 @@ config:
end
end
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@@ -282,7 +347,7 @@ config:
D-->I
D-->I
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@@ -321,7 +386,7 @@ flowchart LR
n8@{ shape: rect}
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@@ -337,7 +402,7 @@ flowchart LR
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@@ -346,7 +411,7 @@ flowchart LR
A{A} --> B & C
</pre
>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk
@@ -376,7 +441,7 @@ flowchart LR
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
kanban:
@@ -395,81 +460,81 @@ kanban
task3[💻 Develop login feature]@{ ticket: 103 }
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' }
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' }
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'square' }
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'square' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'square' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'square' }
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' }
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' }
A:::AClass
classDef AClass fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
flowchart LR
nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' }
style A fill:#f9f,stroke:#333,stroke-width:4px
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
kanban
id2[In progress]
docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
kanban:

View File

@@ -83,6 +83,7 @@
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^15.0.7",
"non-layered-tidy-tree-layout": "^2.0.2",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",

View File

@@ -0,0 +1,281 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
addNodes,
addEdges,
extractPositionedNodes,
extractPositionedEdges,
} from './cytoscape-setup.js';
import type { Node, Edge } from '../../types.js';
// Mock cytoscape
const mockCy = {
add: vi.fn(),
nodes: vi.fn(),
edges: vi.fn(),
};
vi.mock('cytoscape', () => {
const mockCytoscape = vi.fn(() => mockCy) as any;
mockCytoscape.use = vi.fn();
return {
default: mockCytoscape,
};
});
vi.mock('cytoscape-cose-bilkent', () => ({
default: vi.fn(),
}));
vi.mock('d3', () => ({
select: vi.fn(() => ({
append: vi.fn(() => ({
attr: vi.fn(() => ({
attr: vi.fn(() => ({
remove: vi.fn(),
})),
})),
})),
})),
}));
describe('Cytoscape Setup', () => {
let mockNodes: Node[];
let mockEdges: Edge[];
beforeEach(() => {
vi.clearAllMocks();
mockNodes = [
{
id: '1',
label: 'Root',
isGroup: false,
shape: 'rect',
width: 100,
height: 50,
padding: 10,
x: 100,
y: 100,
cssClasses: '',
cssStyles: [],
look: 'default',
},
{
id: '2',
label: 'Child 1',
isGroup: false,
shape: 'rect',
width: 80,
height: 40,
padding: 10,
x: 150,
y: 150,
cssClasses: '',
cssStyles: [],
look: 'default',
},
];
mockEdges = [
{
id: '1_2',
start: '1',
end: '2',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
];
});
describe('addNodes', () => {
it('should add nodes to cytoscape', () => {
addNodes([mockNodes[0]], mockCy as unknown as any);
expect(mockCy.add).toHaveBeenCalledWith({
group: 'nodes',
data: {
id: '1',
labelText: 'Root',
height: 50,
width: 100,
padding: 10,
isGroup: false,
shape: 'rect',
cssClasses: '',
cssStyles: [],
look: 'default',
},
position: {
x: 100,
y: 100,
},
});
});
it('should add multiple nodes to cytoscape', () => {
addNodes(mockNodes, mockCy as unknown as any);
expect(mockCy.add).toHaveBeenCalledTimes(2);
expect(mockCy.add).toHaveBeenCalledWith({
group: 'nodes',
data: {
id: '1',
labelText: 'Root',
height: 50,
width: 100,
padding: 10,
isGroup: false,
shape: 'rect',
cssClasses: '',
cssStyles: [],
look: 'default',
},
position: {
x: 100,
y: 100,
},
});
expect(mockCy.add).toHaveBeenCalledWith({
group: 'nodes',
data: {
id: '2',
labelText: 'Child 1',
height: 40,
width: 80,
padding: 10,
isGroup: false,
shape: 'rect',
cssClasses: '',
cssStyles: [],
look: 'default',
},
position: {
x: 150,
y: 150,
},
});
});
});
describe('addEdges', () => {
it('should add edges to cytoscape', () => {
addEdges(mockEdges, mockCy as unknown as any);
expect(mockCy.add).toHaveBeenCalledWith({
group: 'edges',
data: {
id: '1_2',
source: '1',
target: '2',
type: 'edge',
classes: '',
style: [],
animate: false,
arrowTypeEnd: 'arrow_point',
arrowTypeStart: 'none',
},
});
});
});
describe('extractPositionedNodes', () => {
it('should extract positioned nodes from cytoscape', () => {
const mockCytoscapeNodes = [
{
data: () => ({
id: '1',
labelText: 'Root',
width: 100,
height: 50,
padding: 10,
isGroup: false,
shape: 'rect',
}),
position: () => ({ x: 100, y: 100 }),
},
{
data: () => ({
id: '2',
labelText: 'Child 1',
width: 80,
height: 40,
padding: 10,
isGroup: false,
shape: 'rect',
}),
position: () => ({ x: 150, y: 150 }),
},
];
mockCy.nodes.mockReturnValue({
map: (fn: unknown) => mockCytoscapeNodes.map(fn as any),
});
const result = extractPositionedNodes(mockCy as unknown as any);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: '1',
x: 100,
y: 100,
labelText: 'Root',
width: 100,
height: 50,
padding: 10,
isGroup: false,
shape: 'rect',
});
});
});
describe('extractPositionedEdges', () => {
it('should extract positioned edges from cytoscape', () => {
const mockCytoscapeEdges = [
{
data: () => ({
id: '1_2',
source: '1',
target: '2',
type: 'edge',
}),
_private: {
rscratch: {
startX: 100,
startY: 100,
midX: 125,
midY: 125,
endX: 150,
endY: 150,
},
},
},
];
mockCy.edges.mockReturnValue({
map: (fn: unknown) => mockCytoscapeEdges.map(fn as any),
});
const result = extractPositionedEdges(mockCy as unknown as any);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: '1_2',
source: '1',
target: '2',
type: 'edge',
startX: 100,
startY: 100,
midX: 125,
midY: 125,
endX: 150,
endY: 150,
});
});
});
});

View File

@@ -0,0 +1,207 @@
import cytoscape from 'cytoscape';
import coseBilkent from 'cytoscape-cose-bilkent';
import { select } from 'd3';
import { log } from '../../../logger.js';
import type { LayoutData, Node, Edge } from '../../types.js';
import type { CytoscapeLayoutConfig, PositionedNode, PositionedEdge } from './types.js';
// Inject the layout algorithm into cytoscape
cytoscape.use(coseBilkent);
/**
* Declare module augmentation for cytoscape edge types
*/
declare module 'cytoscape' {
interface EdgeSingular {
_private: {
bodyBounds: unknown;
rscratch: {
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
};
};
}
}
/**
* Add nodes to cytoscape instance from provided node array
* This function processes only the nodes provided in the data structure
* @param nodes - Array of nodes to add
* @param cy - The cytoscape instance
*/
export function addNodes(nodes: Node[], cy: cytoscape.Core): void {
nodes.forEach((node) => {
const nodeData: Record<string, unknown> = {
id: node.id,
labelText: node.label,
height: node.height,
width: node.width,
padding: node.padding ?? 0,
};
// Add any additional properties from the node
Object.keys(node).forEach((key) => {
if (!['id', 'label', 'height', 'width', 'padding', 'x', 'y'].includes(key)) {
nodeData[key] = (node as unknown as Record<string, unknown>)[key];
}
});
cy.add({
group: 'nodes',
data: nodeData,
position: {
x: node.x ?? 0,
y: node.y ?? 0,
},
});
});
}
/**
* Add edges to cytoscape instance from provided edge array
* This function processes only the edges provided in the data structure
* @param edges - Array of edges to add
* @param cy - The cytoscape instance
*/
export function addEdges(edges: Edge[], cy: cytoscape.Core): void {
edges.forEach((edge) => {
const edgeData: Record<string, unknown> = {
id: edge.id,
source: edge.start,
target: edge.end,
};
// Add any additional properties from the edge
Object.keys(edge).forEach((key) => {
if (!['id', 'start', 'end'].includes(key)) {
edgeData[key] = (edge as unknown as Record<string, unknown>)[key];
}
});
cy.add({
group: 'edges',
data: edgeData,
});
});
}
/**
* Create and configure cytoscape instance
* @param data - Layout data containing nodes and edges
* @returns Promise resolving to configured cytoscape instance
*/
export function createCytoscapeInstance(data: LayoutData): Promise<cytoscape.Core> {
return new Promise((resolve) => {
// Add temporary render element
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
const cy = cytoscape({
container: document.getElementById('cy'), // container to render in
style: [
{
selector: 'edge',
style: {
'curve-style': 'bezier',
},
},
],
});
// Remove element after layout
renderEl.remove();
// Add all nodes and edges to cytoscape using the generic functions
addNodes(data.nodes, cy);
addEdges(data.edges, cy);
// Make cytoscape care about the dimensions of the nodes
cy.nodes().forEach(function (n) {
n.layoutDimensions = () => {
const nodeData = n.data();
return { w: nodeData.width, h: nodeData.height };
};
});
// Configure and run the cose-bilkent layout
const layoutConfig: CytoscapeLayoutConfig = {
name: 'cose-bilkent',
// @ts-ignore Types for cose-bilkent are not correct?
quality: 'proof',
styleEnabled: false,
animate: false,
};
cy.layout(layoutConfig).run();
cy.ready((e) => {
log.info('Cytoscape ready', e);
resolve(cy);
});
});
}
/**
* Extract positioned nodes from cytoscape instance
* @param cy - The cytoscape instance after layout
* @returns Array of positioned nodes
*/
export function extractPositionedNodes(cy: cytoscape.Core): PositionedNode[] {
return cy.nodes().map((node) => {
const data = node.data();
const position = node.position();
// Create a positioned node with all original data plus position
const positionedNode: PositionedNode = {
id: data.id,
x: position.x,
y: position.y,
};
// Add all other properties from the original data
Object.keys(data).forEach((key) => {
if (key !== 'id') {
positionedNode[key] = data[key];
}
});
return positionedNode;
});
}
/**
* Extract positioned edges from cytoscape instance
* @param cy - The cytoscape instance after layout
* @returns Array of positioned edges
*/
export function extractPositionedEdges(cy: cytoscape.Core): PositionedEdge[] {
return cy.edges().map((edge) => {
const data = edge.data();
const rscratch = edge._private.rscratch;
// Create a positioned edge with all original data plus position
const positionedEdge: PositionedEdge = {
id: data.id,
source: data.source,
target: data.target,
startX: rscratch.startX,
startY: rscratch.startY,
midX: rscratch.midX,
midY: rscratch.midY,
endX: rscratch.endX,
endY: rscratch.endY,
};
// Add all other properties from the original data
Object.keys(data).forEach((key) => {
if (!['id', 'source', 'target'].includes(key)) {
positionedEdge[key] = data[key];
}
});
return positionedEdge;
});
}

View File

@@ -0,0 +1,25 @@
import { render as renderWithCoseBilkent } from './render.js';
/**
* Cose-Bilkent Layout Algorithm for Generic Diagrams
*
* This module provides a layout algorithm implementation using Cytoscape
* with the cose-bilkent algorithm for positioning nodes and edges.
*
* The algorithm follows the unified rendering pattern and can be used
* by any diagram type that provides compatible LayoutData.
*/
/**
* Render function for the cose-bilkent layout algorithm
*
* This function follows the unified rendering pattern used by all layout algorithms.
* It takes LayoutData, inserts nodes into DOM, runs the cose-bilkent layout algorithm,
* and renders the positioned elements to the SVG.
*
* @param layoutData - Layout data containing nodes, edges, and configuration
* @param svg - SVG element to render to
* @param helpers - Internal helper functions for rendering
* @param options - Rendering options
*/
export const render = renderWithCoseBilkent;

View File

@@ -0,0 +1,250 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock cytoscape and cytoscape-cose-bilkent before importing the modules
vi.mock('cytoscape', () => {
const mockCy = {
add: vi.fn(),
nodes: vi.fn(() => ({
forEach: vi.fn(),
map: vi.fn((fn) => [
fn({
data: () => ({
id: '1',
nodeId: '1',
labelText: 'Root',
level: 0,
type: 0,
width: 100,
height: 50,
padding: 10,
}),
position: () => ({ x: 100, y: 100 }),
}),
]),
})),
edges: vi.fn(() => ({
map: vi.fn((fn) => [
fn({
data: () => ({
id: '1_2',
source: '1',
target: '2',
depth: 0,
}),
_private: {
rscratch: {
startX: 100,
startY: 100,
midX: 150,
midY: 150,
endX: 200,
endY: 200,
},
},
}),
]),
})),
layout: vi.fn(() => ({
run: vi.fn(),
})),
ready: vi.fn((callback) => callback({})),
};
const mockCytoscape = vi.fn(() => mockCy);
mockCytoscape.use = vi.fn();
return {
default: mockCytoscape,
};
});
vi.mock('cytoscape-cose-bilkent', () => ({
default: vi.fn(),
}));
vi.mock('d3', () => ({
select: vi.fn(() => ({
append: vi.fn(() => ({
attr: vi.fn(() => ({
attr: vi.fn(() => ({
remove: vi.fn(),
})),
})),
})),
})),
}));
// Import modules after mocks
import { layout, validateLayoutData } from './index.js';
import type { MindmapLayoutData, LayoutResult } from './types.js';
import type { MindmapNode } from '../../../diagrams/mindmap/mindmapTypes.js';
import type { MermaidConfig } from '../../../config.type.js';
describe('Cose-Bilkent Layout Algorithm', () => {
let mockConfig: MermaidConfig;
let mockRootNode: MindmapNode;
let mockLayoutData: MindmapLayoutData;
beforeEach(() => {
mockConfig = {
mindmap: {
layoutAlgorithm: 'cose-bilkent',
padding: 10,
maxNodeWidth: 200,
useMaxWidth: true,
},
} as MermaidConfig;
mockRootNode = {
id: 1,
nodeId: '1',
level: 0,
descr: 'Root',
type: 0,
width: 100,
height: 50,
padding: 10,
x: 0,
y: 0,
children: [
{
id: 2,
nodeId: '2',
level: 1,
descr: 'Child 1',
type: 0,
width: 80,
height: 40,
padding: 10,
x: 0,
y: 0,
},
],
} as MindmapNode;
mockLayoutData = {
nodes: [
{
id: '1',
nodeId: '1',
level: 0,
descr: 'Root',
type: 0,
width: 100,
height: 50,
padding: 10,
},
{
id: '2',
nodeId: '2',
level: 1,
descr: 'Child 1',
type: 0,
width: 80,
height: 40,
padding: 10,
},
],
edges: [
{
id: '1_2',
source: '1',
target: '2',
depth: 0,
},
],
config: mockConfig,
rootNode: mockRootNode,
};
});
describe('validateLayoutData', () => {
it('should validate correct layout data', () => {
expect(() => validateLayoutData(mockLayoutData)).not.toThrow();
});
it('should throw error for missing data', () => {
expect(() => validateLayoutData(null as any)).toThrow('Layout data is required');
});
it('should throw error for missing root node', () => {
const invalidData = { ...mockLayoutData, rootNode: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Root node is required');
});
it('should throw error for missing config', () => {
const invalidData = { ...mockLayoutData, config: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Configuration is required');
});
it('should throw error for invalid nodes array', () => {
const invalidData = { ...mockLayoutData, nodes: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Nodes array is required');
});
it('should throw error for invalid edges array', () => {
const invalidData = { ...mockLayoutData, edges: null as any };
expect(() => validateLayoutData(invalidData)).toThrow('Edges array is required');
});
});
describe('layout function', () => {
it('should execute layout algorithm successfully', async () => {
const result: LayoutResult = await layout(mockLayoutData, mockConfig);
expect(result).toBeDefined();
expect(result.nodes).toBeDefined();
expect(result.edges).toBeDefined();
expect(Array.isArray(result.nodes)).toBe(true);
expect(Array.isArray(result.edges)).toBe(true);
});
it('should return positioned nodes with coordinates', async () => {
const result: LayoutResult = await layout(mockLayoutData, mockConfig);
expect(result.nodes.length).toBeGreaterThan(0);
result.nodes.forEach((node) => {
expect(node.x).toBeDefined();
expect(node.y).toBeDefined();
expect(typeof node.x).toBe('number');
expect(typeof node.y).toBe('number');
});
});
it('should return positioned edges with coordinates', async () => {
const result: LayoutResult = await layout(mockLayoutData, mockConfig);
expect(result.edges.length).toBeGreaterThan(0);
result.edges.forEach((edge) => {
expect(edge.startX).toBeDefined();
expect(edge.startY).toBeDefined();
expect(edge.midX).toBeDefined();
expect(edge.midY).toBeDefined();
expect(edge.endX).toBeDefined();
expect(edge.endY).toBeDefined();
});
});
it('should handle empty mindmap data gracefully', async () => {
const emptyData: MindmapLayoutData = {
nodes: [],
edges: [],
config: mockConfig,
rootNode: mockRootNode,
};
const result: LayoutResult = await layout(emptyData, mockConfig);
expect(result).toBeDefined();
expect(result.nodes).toBeDefined();
expect(result.edges).toBeDefined();
expect(Array.isArray(result.nodes)).toBe(true);
expect(Array.isArray(result.edges)).toBe(true);
});
it('should throw error for invalid data', async () => {
const invalidData = { ...mockLayoutData, rootNode: null as any };
await expect(layout(invalidData, mockConfig)).rejects.toThrow();
});
});
});

View File

@@ -0,0 +1,79 @@
import type { MermaidConfig } from '../../../config.type.js';
import { log } from '../../../logger.js';
import type { LayoutData } from '../../types.js';
import type { LayoutResult } from './types.js';
import {
createCytoscapeInstance,
extractPositionedNodes,
extractPositionedEdges,
} from './cytoscape-setup.js';
/**
* Execute the cose-bilkent layout algorithm on generic layout data
*
* This function takes layout data and uses Cytoscape with the cose-bilkent
* algorithm to calculate optimal node positions and edge paths.
*
* @param data - The layout data containing nodes, edges, and configuration
* @param config - Mermaid configuration object
* @returns Promise resolving to layout result with positioned nodes and edges
*/
export async function executeCoseBilkentLayout(
data: LayoutData,
_config: MermaidConfig
): Promise<LayoutResult> {
log.debug('Starting cose-bilkent layout algorithm');
try {
// Validate input data
if (!data.nodes || !Array.isArray(data.nodes)) {
throw new Error('No nodes found in layout data');
}
if (!data.edges || !Array.isArray(data.edges)) {
throw new Error('No edges found in layout data');
}
// Create and configure cytoscape instance
const cy = await createCytoscapeInstance(data);
// Extract positioned nodes and edges after layout
const positionedNodes = extractPositionedNodes(cy);
const positionedEdges = extractPositionedEdges(cy);
log.debug(`Layout completed: ${positionedNodes.length} nodes, ${positionedEdges.length} edges`);
return {
nodes: positionedNodes,
edges: positionedEdges,
};
} catch (error) {
log.error('Error in cose-bilkent layout algorithm:', error);
throw error;
}
}
/**
* Validate layout data structure
* @param data - The data to validate
* @returns True if data is valid, throws error otherwise
*/
export function validateLayoutData(data: LayoutData): boolean {
if (!data) {
throw new Error('Layout data is required');
}
if (!data.config) {
throw new Error('Configuration is required in layout data');
}
if (!Array.isArray(data.nodes)) {
throw new Error('Nodes array is required in layout data');
}
if (!Array.isArray(data.edges)) {
throw new Error('Edges array is required in layout data');
}
return true;
}

View File

@@ -0,0 +1,183 @@
import type { InternalHelpers, LayoutData, RenderOptions, SVG, SVGGroup } from 'mermaid';
import { executeCoseBilkentLayout } from './layout.js';
type Node = LayoutData['nodes'][number];
interface NodeWithPosition extends Node {
x?: number;
y?: number;
domId?: SVGGroup;
}
/**
* Render function for cose-bilkent layout algorithm
*
* This follows the same pattern as ELK and dagre renderers:
* 1. Insert nodes into DOM to get their actual dimensions
* 2. Run the layout algorithm to calculate positions
* 3. Position the nodes and edges based on layout results
*/
export const render = async (
data4Layout: LayoutData,
svg: SVG,
{
insertCluster,
insertEdge,
insertEdgeLabel,
insertMarkers,
insertNode,
log,
positionEdgeLabel,
}: InternalHelpers,
{ algorithm }: RenderOptions
) => {
const nodeDb: Record<string, NodeWithPosition> = {};
const clusterDb: Record<string, any> = {};
// Insert markers for edges
const element = svg.select('g');
insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId);
// Create container groups
const subGraphsEl = element.insert('g').attr('class', 'subgraphs');
const edgePaths = element.insert('g').attr('class', 'edgePaths');
const edgeLabels = element.insert('g').attr('class', 'edgeLabels');
const nodes = element.insert('g').attr('class', 'nodes');
// Step 1: Insert nodes into DOM to get their actual dimensions
log.debug('Inserting nodes into DOM for dimension calculation');
await Promise.all(
data4Layout.nodes.map(async (node) => {
if (node.isGroup) {
// Handle subgraphs/clusters
const clusterNode: NodeWithPosition = { ...node };
clusterDb[node.id] = clusterNode;
nodeDb[node.id] = clusterNode;
// Insert cluster to get dimensions
await insertCluster(subGraphsEl, node);
} else {
// Handle regular nodes
const nodeWithPosition: NodeWithPosition = { ...node };
nodeDb[node.id] = nodeWithPosition;
// Insert node to get actual dimensions
const nodeEl = await insertNode(nodes, node, {
config: data4Layout.config,
dir: data4Layout.direction || 'TB',
});
// Get the actual bounding box after insertion
const boundingBox = nodeEl.node()!.getBBox();
nodeWithPosition.width = boundingBox.width;
nodeWithPosition.height = boundingBox.height;
nodeWithPosition.domId = nodeEl;
log.debug(`Node ${node.id} dimensions: ${boundingBox.width}x${boundingBox.height}`);
}
})
);
// Step 2: Run the cose-bilkent layout algorithm
log.debug('Running cose-bilkent layout algorithm');
// Update the layout data with actual dimensions
const updatedLayoutData = {
...data4Layout,
nodes: data4Layout.nodes.map((node) => {
const nodeWithDimensions = nodeDb[node.id];
return {
...node,
width: nodeWithDimensions.width,
height: nodeWithDimensions.height,
};
}),
};
const layoutResult = await executeCoseBilkentLayout(updatedLayoutData, data4Layout.config);
// Step 3: Position the nodes based on layout results
log.debug('Positioning nodes based on layout results');
layoutResult.nodes.forEach((positionedNode) => {
const node = nodeDb[positionedNode.id];
if (node && node.domId) {
// Position the node at the calculated coordinates
// The positionedNode.x/y represents the center of the node, so use directly
node.domId.attr('transform', `translate(${positionedNode.x}, ${positionedNode.y})`);
// Store the final position
node.x = positionedNode.x;
node.y = positionedNode.y;
log.debug(`Positioned node ${node.id} at center (${positionedNode.x}, ${positionedNode.y})`);
}
});
// Step 4: Insert and position edges
log.debug('Inserting and positioning edges');
await Promise.all(
data4Layout.edges.map(async (edge) => {
// Insert edge label first
const edgeLabel = await insertEdgeLabel(edgeLabels, edge);
// Get start and end nodes
const startNode = nodeDb[edge.start];
const endNode = nodeDb[edge.end];
if (startNode && endNode) {
// Find the positioned edge data
const positionedEdge = layoutResult.edges.find((e) => e.id === edge.id);
if (positionedEdge) {
// Create edge path with positioned coordinates
const edgeWithPath = {
...edge,
points: [
{ x: positionedEdge.startX, y: positionedEdge.startY },
{ x: positionedEdge.endX, y: positionedEdge.endY },
],
};
// Insert the edge path
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
// Position the edge label
positionEdgeLabel(edgeWithPath, paths);
} else {
// Fallback: create a simple straight line between nodes
const edgeWithPath = {
...edge,
points: [
{ x: startNode.x || 0, y: startNode.y || 0 },
{ x: endNode.x || 0, y: endNode.y || 0 },
],
};
const paths = insertEdge(
edgePaths,
edgeWithPath,
clusterDb,
data4Layout.type,
startNode,
endNode,
data4Layout.diagramId
);
positionEdgeLabel(edgeWithPath, paths);
}
}
})
);
log.debug('Cose-bilkent rendering completed');
};

View File

@@ -0,0 +1,43 @@
/**
* Positioned node after layout calculation
*/
export interface PositionedNode {
id: string;
x: number;
y: number;
[key: string]: unknown; // Allow additional properties
}
/**
* Positioned edge after layout calculation
*/
export interface PositionedEdge {
id: string;
source: string;
target: string;
startX: number;
startY: number;
midX: number;
midY: number;
endX: number;
endY: number;
[key: string]: unknown; // Allow additional properties
}
/**
* Result of layout algorithm execution
*/
export interface LayoutResult {
nodes: PositionedNode[];
edges: PositionedEdge[];
}
/**
* Cytoscape layout configuration
*/
export interface CytoscapeLayoutConfig {
name: 'cose-bilkent';
quality: 'proof';
styleEnabled: boolean;
animate: boolean;
}

View File

@@ -43,6 +43,10 @@ const registerDefaultLayoutLoaders = () => {
name: 'cose-bilkent',
loader: async () => await import('./layout-algorithms/cose-bilkent/index.js'),
},
{
name: 'tidy-tree',
loader: async () => await import('./layout-algorithms/tidy-tree/index.js'),
},
]);
};

309
pnpm-lock.yaml generated
View File

@@ -265,6 +265,9 @@ importers:
marked:
specifier: ^15.0.7
version: 15.0.7
non-layered-tidy-tree-layout:
specifier: ^2.0.2
version: 2.0.2
roughjs:
specifier: ^4.6.6
version: 4.6.6(patch_hash=3543d47108cb41b68ec6a671c0e1f9d0cfe2ce524fea5b0992511ae84c3c6b64)
@@ -508,6 +511,67 @@ importers:
specifier: ^7.3.0
version: 7.3.0
packages/mermaid/src/vitepress:
dependencies:
'@mdi/font':
specifier: ^7.4.47
version: 7.4.47
'@vueuse/core':
specifier: ^12.7.0
version: 12.7.0(typescript@5.7.3)
font-awesome:
specifier: ^4.7.0
version: 4.7.0
jiti:
specifier: ^2.4.2
version: 2.4.2
mermaid:
specifier: workspace:^
version: link:../..
vue:
specifier: ^3.4.38
version: 3.5.13(typescript@5.7.3)
devDependencies:
'@iconify-json/carbon':
specifier: ^1.1.37
version: 1.2.1
'@unocss/reset':
specifier: ^66.0.0
version: 66.0.0
'@vite-pwa/vitepress':
specifier: ^0.5.3
version: 0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))
'@vitejs/plugin-vue':
specifier: ^5.0.5
version: 5.2.1(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
fast-glob:
specifier: ^3.3.3
version: 3.3.3
https-localhost:
specifier: ^4.7.1
version: 4.7.1
pathe:
specifier: ^2.0.3
version: 2.0.3
unocss:
specifier: ^66.0.0
version: 66.0.0(postcss@8.5.3)(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.4.0(@babel/parser@7.27.2)(vue@3.5.13(typescript@5.7.3))
vite:
specifier: ^6.1.1
version: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
vite-plugin-pwa:
specifier: ^0.21.1
version: 0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vitepress:
specifier: 1.6.3
version: 1.6.3(@algolia/client-search@5.20.3)(@types/node@22.13.5)(axios@1.8.4)(postcss@8.5.3)(search-insights@2.17.2)(terser@5.39.0)(typescript@5.7.3)
workbox-window:
specifier: ^7.3.0
version: 7.3.0
packages/parser:
dependencies:
langium:
@@ -912,10 +976,6 @@ packages:
resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.25.9':
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
@@ -3627,6 +3687,15 @@ packages:
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
'@vite-pwa/vitepress@0.5.4':
resolution: {integrity: sha512-g57qwG983WTyQNLnOcDVPQEIeN+QDgK/HdqghmygiUFp3a/MzVvmLXC/EVnPAXxWa8W2g9pZ9lE3EiDGs2HjsA==}
peerDependencies:
'@vite-pwa/assets-generator': ^0.2.6
vite-plugin-pwa: '>=0.21.2 <1'
peerDependenciesMeta:
'@vite-pwa/assets-generator':
optional: true
'@vite-pwa/vitepress@1.0.0':
resolution: {integrity: sha512-i5RFah4urA6tZycYlGyBslVx8cVzbZBcARJLDg5rWMfAkRmyLtpRU6usGfVOwyN9kjJ2Bkm+gBHXF1hhr7HptQ==}
peerDependencies:
@@ -4372,10 +4441,6 @@ packages:
resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
engines: {node: '>= 0.4'}
call-bound@1.0.3:
resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
@@ -6082,10 +6147,6 @@ packages:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'}
get-intrinsic@1.2.7:
resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==}
engines: {node: '>= 0.4'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -7517,10 +7578,6 @@ packages:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-db@1.53.0:
resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==}
engines: {node: '>= 0.6'}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
@@ -7714,6 +7771,9 @@ packages:
resolution: {integrity: sha512-fiVbT7BqxiQqjlR9U3FDGOSERFCKoXVCdxV2FwZuNN7/cmJ42iQx35nUFOAFDcyvemu9Adp+IlsCGlKQYLmBKw==}
deprecated: Package no longer supported. Contact support@npmjs.com for more info.
non-layered-tidy-tree-layout@2.0.2:
resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -9594,6 +9654,18 @@ packages:
peerDependencies:
vite: '>=4 <=6'
vite-plugin-pwa@0.21.2:
resolution: {integrity: sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg==}
engines: {node: '>=16.0.0'}
peerDependencies:
'@vite-pwa/assets-generator': ^0.2.6
vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
workbox-build: ^7.3.0
workbox-window: ^7.3.0
peerDependenciesMeta:
'@vite-pwa/assets-generator':
optional: true
vite-plugin-pwa@1.0.0:
resolution: {integrity: sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==}
engines: {node: '>=16.0.0'}
@@ -10648,7 +10720,7 @@ snapshots:
'@babel/code-frame@7.26.2':
dependencies:
'@babel/helper-validator-identifier': 7.25.9
'@babel/helper-validator-identifier': 7.27.1
js-tokens: 4.0.0
picocolors: 1.1.1
@@ -10670,10 +10742,10 @@ snapshots:
'@babel/helper-compilation-targets': 7.26.5
'@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.9)
'@babel/helpers': 7.26.9
'@babel/parser': 7.26.9
'@babel/parser': 7.27.2
'@babel/template': 7.26.9
'@babel/traverse': 7.26.9
'@babel/types': 7.26.9
'@babel/types': 7.27.1
convert-source-map: 2.0.0
debug: 4.4.0(supports-color@8.1.1)
gensync: 1.0.0-beta.2
@@ -10704,8 +10776,8 @@ snapshots:
'@babel/generator@7.26.9':
dependencies:
'@babel/parser': 7.26.9
'@babel/types': 7.26.9
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.1.0
@@ -10810,7 +10882,7 @@ snapshots:
'@babel/helper-module-imports@7.25.9':
dependencies:
'@babel/traverse': 7.26.9
'@babel/types': 7.26.9
'@babel/types': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10825,7 +10897,7 @@ snapshots:
dependencies:
'@babel/core': 7.26.9
'@babel/helper-module-imports': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.26.9
transitivePeerDependencies:
- supports-color
@@ -10901,8 +10973,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/helper-string-parser@7.25.9': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.25.9': {}
@@ -10924,7 +10994,7 @@ snapshots:
'@babel/helpers@7.26.9':
dependencies:
'@babel/template': 7.26.9
'@babel/types': 7.26.9
'@babel/types': 7.27.1
'@babel/helpers@7.27.1':
dependencies:
@@ -10933,7 +11003,7 @@ snapshots:
'@babel/parser@7.26.9':
dependencies:
'@babel/types': 7.26.9
'@babel/types': 7.27.1
'@babel/parser@7.27.2':
dependencies:
@@ -11945,8 +12015,8 @@ snapshots:
'@babel/template@7.26.9':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/parser': 7.26.9
'@babel/types': 7.26.9
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@babel/template@7.27.2':
dependencies:
@@ -11958,9 +12028,9 @@ snapshots:
dependencies:
'@babel/code-frame': 7.26.2
'@babel/generator': 7.26.9
'@babel/parser': 7.26.9
'@babel/parser': 7.27.2
'@babel/template': 7.26.9
'@babel/types': 7.26.9
'@babel/types': 7.27.1
debug: 4.4.0(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
@@ -11980,8 +12050,8 @@ snapshots:
'@babel/types@7.26.9':
dependencies:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/types@7.27.1':
dependencies:
@@ -13634,24 +13704,24 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.26.9
'@babel/types': 7.26.9
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@types/babel__generator': 7.6.8
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.20.6
'@types/babel__generator@7.6.8':
dependencies:
'@babel/types': 7.26.9
'@babel/types': 7.27.1
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.26.9
'@babel/types': 7.26.9
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@types/babel__traverse@7.20.6':
dependencies:
'@babel/types': 7.26.9
'@babel/types': 7.27.1
'@types/body-parser@1.19.5':
dependencies:
@@ -14191,6 +14261,16 @@ snapshots:
transitivePeerDependencies:
- vue
'@unocss/astro@66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@unocss/core': 66.0.0
'@unocss/reset': 66.0.0
'@unocss/vite': 66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
optionalDependencies:
vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
transitivePeerDependencies:
- vue
'@unocss/cli@66.0.0':
dependencies:
'@ampproject/remapping': 2.3.0
@@ -14326,6 +14406,24 @@ snapshots:
transitivePeerDependencies:
- vue
'@unocss/vite@66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@ampproject/remapping': 2.3.0
'@unocss/config': 66.0.0
'@unocss/core': 66.0.0
'@unocss/inspector': 66.0.0(vue@3.5.13(typescript@5.7.3))
chokidar: 3.6.0
magic-string: 0.30.17
tinyglobby: 0.2.12
unplugin-utils: 0.2.4
vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
transitivePeerDependencies:
- vue
'@vite-pwa/vitepress@0.5.4(vite-plugin-pwa@0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
'@vite-pwa/vitepress@1.0.0(vite-plugin-pwa@1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0))':
dependencies:
vite-plugin-pwa: 1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0)
@@ -14340,6 +14438,11 @@ snapshots:
vite: 6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
vue: 3.5.13(typescript@5.7.3)
'@vitejs/plugin-vue@5.2.1(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))':
dependencies:
vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
vue: 3.5.13(typescript@5.7.3)
'@vitest/coverage-v8@3.0.6(vitest@3.0.6)':
dependencies:
'@ampproject/remapping': 2.3.0
@@ -14418,7 +14521,7 @@ snapshots:
'@vue/compiler-core@3.5.13':
dependencies:
'@babel/parser': 7.26.9
'@babel/parser': 7.27.2
'@vue/shared': 3.5.13
entities: 4.5.0
estree-walker: 2.0.2
@@ -14431,7 +14534,7 @@ snapshots:
'@vue/compiler-sfc@3.5.13':
dependencies:
'@babel/parser': 7.26.9
'@babel/parser': 7.27.2
'@vue/compiler-core': 3.5.13
'@vue/compiler-dom': 3.5.13
'@vue/compiler-ssr': 3.5.13
@@ -14884,7 +14987,7 @@ snapshots:
array-buffer-byte-length@1.0.2:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
is-array-buffer: 3.0.5
array-flatten@1.1.1: {}
@@ -14989,7 +15092,7 @@ snapshots:
babel-plugin-jest-hoist@29.6.3:
dependencies:
'@babel/template': 7.26.9
'@babel/types': 7.26.9
'@babel/types': 7.27.1
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.20.6
@@ -15233,14 +15336,9 @@ snapshots:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
get-intrinsic: 1.2.7
get-intrinsic: 1.3.0
set-function-length: 1.2.2
call-bound@1.0.3:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.2.7
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -15508,7 +15606,7 @@ snapshots:
compressible@2.0.18:
dependencies:
mime-db: 1.53.0
mime-db: 1.54.0
compression@1.7.4:
dependencies:
@@ -16175,7 +16273,7 @@ snapshots:
array-buffer-byte-length: 1.0.2
call-bind: 1.0.8
es-get-iterator: 1.1.3
get-intrinsic: 1.2.7
get-intrinsic: 1.3.0
is-arguments: 1.1.1
is-array-buffer: 3.0.5
is-date-object: 1.1.0
@@ -16498,7 +16596,7 @@ snapshots:
es-get-iterator@1.1.3:
dependencies:
call-bind: 1.0.8
get-intrinsic: 1.2.7
get-intrinsic: 1.3.0
has-symbols: 1.1.0
is-arguments: 1.1.1
is-map: 2.0.3
@@ -16518,7 +16616,7 @@ snapshots:
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.2.7
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
@@ -17214,7 +17312,7 @@ snapshots:
find-test-names@1.29.5(@babel/core@7.26.9):
dependencies:
'@babel/parser': 7.26.9
'@babel/parser': 7.27.2
'@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.26.9)
acorn-walk: 8.3.4
debug: 4.4.0(supports-color@8.1.1)
@@ -17376,19 +17474,6 @@ snapshots:
get-east-asian-width@1.3.0: {}
get-intrinsic@1.2.7:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -17842,8 +17927,8 @@ snapshots:
is-array-buffer@3.0.5:
dependencies:
call-bind: 1.0.8
call-bound: 1.0.3
get-intrinsic: 1.2.7
call-bound: 1.0.4
get-intrinsic: 1.3.0
is-arrayish@0.2.1: {}
@@ -17867,7 +17952,7 @@ snapshots:
is-boolean-object@1.2.2:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-builtin-module@5.0.0:
@@ -17888,7 +17973,7 @@ snapshots:
is-date-object@1.1.0:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-decimal@1.0.4: {}
@@ -17937,7 +18022,7 @@ snapshots:
is-number-object@1.1.1:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-number@7.0.0: {}
@@ -17960,7 +18045,7 @@ snapshots:
is-regex@1.2.1:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
gopd: 1.2.0
has-tostringtag: 1.0.2
hasown: 2.0.2
@@ -17971,7 +18056,7 @@ snapshots:
is-shared-array-buffer@1.0.4:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
is-stream@1.1.0: {}
@@ -17981,7 +18066,7 @@ snapshots:
is-string@1.1.1:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-subdir@1.2.0:
@@ -17990,7 +18075,7 @@ snapshots:
is-symbol@1.1.1:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
has-symbols: 1.1.0
safe-regex-test: 1.1.0
@@ -18015,7 +18100,7 @@ snapshots:
is-weakset@2.0.3:
dependencies:
call-bind: 1.0.8
get-intrinsic: 1.2.7
get-intrinsic: 1.3.0
is-what@4.1.16: {}
@@ -18053,7 +18138,7 @@ snapshots:
istanbul-lib-instrument@5.2.1:
dependencies:
'@babel/core': 7.26.9
'@babel/parser': 7.26.9
'@babel/parser': 7.27.2
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
semver: 6.3.1
@@ -18063,7 +18148,7 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
'@babel/core': 7.26.9
'@babel/parser': 7.26.9
'@babel/parser': 7.27.2
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
semver: 7.7.1
@@ -18382,7 +18467,7 @@ snapshots:
'@babel/generator': 7.26.9
'@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.26.9)
'@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.26.9)
'@babel/types': 7.26.9
'@babel/types': 7.27.1
'@jest/expect-utils': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
@@ -19246,8 +19331,6 @@ snapshots:
mime-db@1.52.0: {}
mime-db@1.53.0: {}
mime-db@1.54.0: {}
mime-types@2.1.35:
@@ -19392,13 +19475,15 @@ snapshots:
node-source-walk@7.0.0:
dependencies:
'@babel/parser': 7.26.9
'@babel/parser': 7.27.2
nomnom@1.5.2:
dependencies:
colors: 0.5.1
underscore: 1.1.7
non-layered-tidy-tree-layout@2.0.2: {}
normalize-path@3.0.0: {}
normalize-url@6.1.0: {}
@@ -19497,7 +19582,7 @@ snapshots:
object.assign@4.1.7:
dependencies:
call-bind: 1.0.8
call-bound: 1.0.3
call-bound: 1.0.4
define-properties: 1.2.1
es-object-atoms: 1.1.1
has-symbols: 1.1.0
@@ -20388,7 +20473,7 @@ snapshots:
safe-regex-test@1.1.0:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
es-errors: 1.3.0
is-regex: 1.2.1
@@ -20524,7 +20609,7 @@ snapshots:
define-data-property: 1.1.4
es-errors: 1.3.0
function-bind: 1.1.2
get-intrinsic: 1.2.7
get-intrinsic: 1.3.0
gopd: 1.2.0
has-property-descriptors: 1.0.2
@@ -20613,16 +20698,16 @@ snapshots:
side-channel-map@1.0.1:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.2.7
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.3
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.2.7
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-map: 1.0.1
@@ -21085,7 +21170,7 @@ snapshots:
terser@5.34.1:
dependencies:
'@jridgewell/source-map': 0.3.6
acorn: 8.14.0
acorn: 8.14.1
commander: 2.20.3
source-map-support: 0.5.21
@@ -21455,6 +21540,33 @@ snapshots:
- supports-color
- vue
unocss@66.0.0(postcss@8.5.3)(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@unocss/astro': 66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
'@unocss/cli': 66.0.0
'@unocss/core': 66.0.0
'@unocss/postcss': 66.0.0(postcss@8.5.3)
'@unocss/preset-attributify': 66.0.0
'@unocss/preset-icons': 66.0.0
'@unocss/preset-mini': 66.0.0
'@unocss/preset-tagify': 66.0.0
'@unocss/preset-typography': 66.0.0
'@unocss/preset-uno': 66.0.0
'@unocss/preset-web-fonts': 66.0.0
'@unocss/preset-wind': 66.0.0
'@unocss/preset-wind3': 66.0.0
'@unocss/transformer-attributify-jsx': 66.0.0
'@unocss/transformer-compile-class': 66.0.0
'@unocss/transformer-directives': 66.0.0
'@unocss/transformer-variant-group': 66.0.0
'@unocss/vite': 66.0.0(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.7.3))
optionalDependencies:
vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
transitivePeerDependencies:
- postcss
- supports-color
- vue
unpipe@1.0.0: {}
unplugin-utils@0.2.4:
@@ -21480,7 +21592,7 @@ snapshots:
unplugin@2.2.0:
dependencies:
acorn: 8.14.0
acorn: 8.14.1
webpack-virtual-modules: 0.6.2
untildify@4.0.0: {}
@@ -21570,6 +21682,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@0.21.2(vite@6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.0(supports-color@8.1.1)
pretty-bytes: 6.1.1
tinyglobby: 0.2.12
vite: 6.1.6(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
workbox-build: 7.1.1(@types/babel__core@7.20.5)
workbox-window: 7.3.0
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.0.0(vite@6.1.1(@types/node@22.13.5)(jiti@2.4.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.3.0):
dependencies:
debug: 4.4.0(supports-color@8.1.1)
@@ -22035,7 +22158,7 @@ snapshots:
dependencies:
available-typed-arrays: 1.0.7
call-bind: 1.0.8
call-bound: 1.0.3
call-bound: 1.0.4
for-each: 0.3.5
gopd: 1.2.0
has-tostringtag: 1.0.2