wwf
2025-05-20 938c3e5a587ce950a94964ea509b9e7f8834dfae
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
export function cleanUpSvgCode(svgCode: string): string {
  return svgCode.replaceAll('<br>', '<br/>')
}
 
/**
 * Preprocesses mermaid code to fix common syntax issues
 */
export function preprocessMermaidCode(code: string): string {
  if (!code || typeof code !== 'string')
    return ''
 
  // First check if this is a gantt chart
  if (code.trim().startsWith('gantt')) {
    // For gantt charts, we need to ensure each task is on its own line
    // Split the code into lines and process each line separately
    const lines = code.split('\n').map(line => line.trim())
    return lines.join('\n')
  }
 
  return code
    // Replace English colons with Chinese colons in section nodes to avoid parsing issues
    .replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}:`)
    // Fix common syntax issues
    .replace(/fifopacket/g, 'rect')
    // Clean up empty lines and extra spaces
    .trim()
}
 
/**
 * Prepares mermaid code based on selected style
 */
export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string {
  let finalCode = preprocessMermaidCode(code)
 
  // Special handling for gantt charts
  if (finalCode.trim().startsWith('gantt')) {
    // For gantt charts, preserve the structure exactly as is
    return finalCode
  }
 
  if (style === 'handDrawn') {
    finalCode = finalCode
      // Remove style definitions that interfere with hand-drawn style
      .replace(/style\s+[^\n]+/g, '')
      .replace(/linkStyle\s+[^\n]+/g, '')
      .replace(/^flowchart/, 'graph')
      // Remove any styles that might interfere with hand-drawn style
      .replace(/class="[^"]*"/g, '')
      .replace(/fill="[^"]*"/g, '')
      .replace(/stroke="[^"]*"/g, '')
 
    // Ensure hand-drawn style charts always start with graph
    if (!finalCode.startsWith('graph') && !finalCode.startsWith('flowchart'))
      finalCode = `graph TD\n${finalCode}`
  }
 
  return finalCode
}
 
/**
 * Converts SVG to base64 string for image rendering
 */
export function svgToBase64(svgGraph: string): Promise<string> {
  if (!svgGraph)
    return Promise.resolve('')
 
  try {
    // Ensure SVG has correct XML declaration
    if (!svgGraph.includes('<?xml'))
      svgGraph = `<?xml version="1.0" encoding="UTF-8"?>${svgGraph}`
 
    const blob = new Blob([new TextEncoder().encode(svgGraph)], { type: 'image/svg+xml;charset=utf-8' })
    return new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.onloadend = () => resolve(reader.result as string)
      reader.onerror = reject
      reader.readAsDataURL(blob)
    })
  }
  catch (error) {
    console.error('Error converting SVG to base64:', error)
    return Promise.resolve('')
  }
}
 
/**
 * Processes SVG for theme styling
 */
export function processSvgForTheme(
  svg: string,
  isDark: boolean,
  isHandDrawn: boolean,
  themes: {
    light: any
    dark: any
  },
): string {
  let processedSvg = svg
 
  if (isDark) {
    processedSvg = processedSvg
      .replace(/style="fill: ?#000000"/g, 'style="fill: #e2e8f0"')
      .replace(/style="stroke: ?#000000"/g, 'style="stroke: #94a3b8"')
      .replace(/<rect [^>]*fill="#ffffff"/g, '<rect $& fill="#1e293b"')
 
    if (isHandDrawn) {
      processedSvg = processedSvg
        .replace(/fill="#[a-fA-F0-9]{6}"/g, `fill="${themes.dark.nodeColors[0].bg}"`)
        .replace(/stroke="#[a-fA-F0-9]{6}"/g, `stroke="${themes.dark.connectionColor}"`)
        .replace(/stroke-width="1"/g, 'stroke-width="1.5"')
    }
    else {
      let i = 0
      themes.dark.nodeColors.forEach(() => {
        const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
        processedSvg = processedSvg.replace(regex, (match: string) => {
          const colorIndex = i % themes.dark.nodeColors.length
          i++
          return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.dark.nodeColors[colorIndex].bg}"`)
        })
      })
 
      processedSvg = processedSvg
        .replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
          `<path stroke="${themes.dark.connectionColor}" stroke-width="1.5"`)
        .replace(/<(line|polyline) [^>]*stroke="#[a-fA-F0-9]{6}"/g,
          `<$1 stroke="${themes.dark.connectionColor}" stroke-width="1.5"`)
    }
  }
  else {
    if (isHandDrawn) {
      processedSvg = processedSvg
        .replace(/fill="#[a-fA-F0-9]{6}"/g, `fill="${themes.light.nodeColors[0].bg}"`)
        .replace(/stroke="#[a-fA-F0-9]{6}"/g, `stroke="${themes.light.connectionColor}"`)
        .replace(/stroke-width="1"/g, 'stroke-width="1.5"')
    }
    else {
      themes.light.nodeColors.forEach(() => {
        const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
        let i = 0
        processedSvg = processedSvg.replace(regex, (match: string) => {
          const colorIndex = i % themes.light.nodeColors.length
          i++
          return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.light.nodeColors[colorIndex].bg}"`)
        })
      })
 
      processedSvg = processedSvg
        .replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
          `<path stroke="${themes.light.connectionColor}"`)
        .replace(/<(line|polyline) [^>]*stroke="#[a-fA-F0-9]{6}"/g,
          `<$1 stroke="${themes.light.connectionColor}"`)
    }
  }
 
  return processedSvg
}
 
/**
 * Checks if mermaid code is complete and valid
 */
export function isMermaidCodeComplete(code: string): boolean {
  if (!code || code.trim().length === 0)
    return false
 
  try {
    const trimmedCode = code.trim()
 
    // Special handling for gantt charts
    if (trimmedCode.startsWith('gantt')) {
      // For gantt charts, check if it has at least a title and one task
      const lines = trimmedCode.split('\n').filter(line => line.trim().length > 0)
      return lines.length >= 3
    }
 
    // Check for basic syntax structure
    const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram)/.test(trimmedCode)
 
    // Check for balanced brackets and parentheses
    const isBalanced = (() => {
      const stack = []
      const pairs = { '{': '}', '[': ']', '(': ')' }
 
      for (const char of trimmedCode) {
        if (char in pairs) {
          stack.push(char)
        }
        else if (Object.values(pairs).includes(char)) {
          const last = stack.pop()
          if (pairs[last as keyof typeof pairs] !== char)
            return false
        }
      }
 
      return stack.length === 0
    })()
 
    // Check for common syntax errors
    const hasNoSyntaxErrors = !trimmedCode.includes('undefined')
                           && !trimmedCode.includes('[object Object]')
                           && trimmedCode.split('\n').every(line =>
                             !(line.includes('-->') && !line.match(/\S+\s*-->\s*\S+/)))
 
    return hasValidStart && isBalanced && hasNoSyntaxErrors
  }
  catch (error) {
    console.debug('Mermaid code validation error:', error)
    return false
  }
}
 
/**
 * Helper to wait for DOM element with retry mechanism
 */
export function waitForDOMElement(callback: () => Promise<any>, maxAttempts = 3, delay = 100): Promise<any> {
  return new Promise((resolve, reject) => {
    let attempts = 0
    const tryRender = async () => {
      try {
        resolve(await callback())
      }
      catch (error) {
        attempts++
        if (attempts < maxAttempts)
          setTimeout(tryRender, delay)
        else
          reject(error)
      }
    }
    tryRender()
  })
}