forked from finos/architecture-as-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmermaid-renderer.ts
More file actions
179 lines (153 loc) · 6.84 KB
/
mermaid-renderer.ts
File metadata and controls
179 lines (153 loc) · 6.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import MarkdownIt from 'markdown-it'
import mermaid from 'mermaid'
import { PanZoomManager, PanZoomOptions } from './pan-zoom-manager'
export default class MermaidRenderer {
private md: MarkdownIt
private mermaidReady = false
private panZoomManagers: Map<string, PanZoomManager> = new Map()
constructor() {
this.md = new MarkdownIt({
html: true,
linkify: true,
breaks: true,
})
}
private ensureMermaid() {
if (!this.mermaidReady) {
mermaid.initialize({
startOnLoad: false,
securityLevel: 'strict',
theme: 'base',
deterministicIds: true, // For better performance with large diagrams
logLevel: 'error', // Reduce logging for better performance
flowchart: {
// Better handling of long labels
curve: 'basis',
padding: 15, // Reduced for more compact layouts
nodeSpacing: 40, // Reduced for denser layouts
rankSpacing: 60, // Reduced for more compact vertical spacing
// Allow wrapping for edge labels
htmlLabels: true,
// Improve text wrapping
useMaxWidth: true
},
// Global settings for better rendering of large files
maxTextSize: 2000000, // 2MB to support large architecture files
maxEdges: 10000 // Support complex diagrams with many connections
})
this.mermaidReady = true
}
}
/**
* Render markdown content with Mermaid support
*/
async render(content: string, _sourceFile: string): Promise<string> {
this.ensureMermaid()
try {
// First, check for raw Mermaid blocks in markdown (```mermaid)
const rawMermaidRegex = /```mermaid\s*\n([\s\S]*?)```/g
let processedContent = content
// Process raw Mermaid blocks first
let match
while ((match = rawMermaidRegex.exec(content)) !== null) {
const mermaidCode = match[1].trim()
try {
// Generate a unique ID for this diagram
const diagramId = `mermaid-${Math.random().toString(36).substr(2, 9)}`
// Render the Mermaid diagram
const { svg } = await mermaid.render(diagramId, mermaidCode)
// Wrap the SVG in a container for pan/zoom controls
const wrappedSvg = this.wrapSvgWithContainer(svg, diagramId)
// Replace the code block with the wrapped SVG
processedContent = processedContent.replace(match[0], wrappedSvg)
} catch (error) {
console.error('Error rendering Mermaid diagram:', error)
// Keep the original code block if rendering fails
}
}
// Then, render the remaining markdown using markdown-it
let html = this.md.render(processedContent)
// Finally, check for any remaining HTML-encoded Mermaid blocks
const htmlMermaidRegex = /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g
while ((match = htmlMermaidRegex.exec(html)) !== null) {
// Decode HTML entities before rendering
const encodedCode = match[1].trim()
const mermaidCode = this.decodeHtmlEntities(encodedCode)
try {
// Generate a unique ID for this diagram
const diagramId = `mermaid-${Math.random().toString(36).substr(2, 9)}`
// Render the Mermaid diagram
const { svg } = await mermaid.render(diagramId, mermaidCode)
// Wrap the SVG in a container for pan/zoom controls
const wrappedSvg = this.wrapSvgWithContainer(svg, diagramId)
// Replace the code block with the wrapped SVG
html = html.replace(match[0], wrappedSvg)
} catch (error) {
console.error('Error rendering HTML Mermaid diagram:', error)
// Keep the original code block if rendering fails
}
}
return html
} catch (error) {
console.error('Error rendering markdown:', error)
return `<div style="color: red;">Error rendering content: ${String(error)}</div>`
}
}
/**
* Wrap SVG in a container with a data attribute for pan/zoom initialization
*/
private wrapSvgWithContainer(svg: string, diagramId: string): string {
return `<div class="mermaid-diagram-container" data-diagram-id="${diagramId}">
${svg}
</div>`
}
/**
* Decode HTML entities from markdown-it output using DOMParser for security
*/
private decodeHtmlEntities(text: string): string {
const parser = new DOMParser()
const doc = parser.parseFromString(text, 'text/html')
return doc.body.textContent || ''
}
/**
* Initialize pan/zoom on a diagram after it's been rendered to the DOM
* This should be called from the view after the HTML is inserted
*/
public initializePanZoom(containerId: string, options?: PanZoomOptions): PanZoomManager | null {
const container = document.querySelector(`[data-diagram-id="${containerId}"]`)
if (!container) {
console.warn(`Container not found for diagram ID: ${containerId}`)
return null
}
const svgElement = container.querySelector('svg')
if (!svgElement) {
console.warn(`SVG element not found in container: ${containerId}`)
return null
}
// Remove any max-width/max-height constraints from Mermaid
svgElement.style.removeProperty('max-width')
svgElement.style.removeProperty('max-height')
// Ensure the SVG fills the container
svgElement.style.width = '100%'
svgElement.style.height = '100%'
// Create and initialize pan/zoom manager
const panZoomManager = new PanZoomManager()
panZoomManager.initialize(svgElement, options)
// Store for later cleanup
this.panZoomManagers.set(containerId, panZoomManager)
return panZoomManager
}
/**
* Clean up all pan/zoom instances
*/
public destroyAllPanZoom(): void {
this.panZoomManagers.forEach(manager => manager.destroy())
this.panZoomManagers.clear()
}
/**
* Get pan/zoom manager for a specific diagram
*/
public getPanZoomManager(diagramId: string): PanZoomManager | undefined {
return this.panZoomManagers.get(diagramId)
}
}