You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. export function cleanUpSvgCode(svgCode: string): string {
  2. return svgCode.replaceAll('<br>', '<br/>')
  3. }
  4. /**
  5. * Preprocesses mermaid code to fix common syntax issues
  6. */
  7. export function preprocessMermaidCode(code: string): string {
  8. if (!code || typeof code !== 'string')
  9. return ''
  10. // First check if this is a gantt chart
  11. if (code.trim().startsWith('gantt')) {
  12. // For gantt charts, we need to ensure each task is on its own line
  13. // Split the code into lines and process each line separately
  14. const lines = code.split('\n').map(line => line.trim())
  15. return lines.join('\n')
  16. }
  17. return code
  18. // Replace English colons with Chinese colons in section nodes to avoid parsing issues
  19. .replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}:`)
  20. // Fix common syntax issues
  21. .replace(/fifopacket/g, 'rect')
  22. // Clean up empty lines and extra spaces
  23. .trim()
  24. }
  25. /**
  26. * Prepares mermaid code based on selected style
  27. */
  28. export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string {
  29. let finalCode = preprocessMermaidCode(code)
  30. // Special handling for gantt charts
  31. if (finalCode.trim().startsWith('gantt')) {
  32. // For gantt charts, preserve the structure exactly as is
  33. return finalCode
  34. }
  35. if (style === 'handDrawn') {
  36. finalCode = finalCode
  37. // Remove style definitions that interfere with hand-drawn style
  38. .replace(/style\s+[^\n]+/g, '')
  39. .replace(/linkStyle\s+[^\n]+/g, '')
  40. .replace(/^flowchart/, 'graph')
  41. // Remove any styles that might interfere with hand-drawn style
  42. .replace(/class="[^"]*"/g, '')
  43. .replace(/fill="[^"]*"/g, '')
  44. .replace(/stroke="[^"]*"/g, '')
  45. // Ensure hand-drawn style charts always start with graph
  46. if (!finalCode.startsWith('graph') && !finalCode.startsWith('flowchart'))
  47. finalCode = `graph TD\n${finalCode}`
  48. }
  49. return finalCode
  50. }
  51. /**
  52. * Converts SVG to base64 string for image rendering
  53. */
  54. export function svgToBase64(svgGraph: string): Promise<string> {
  55. if (!svgGraph)
  56. return Promise.resolve('')
  57. try {
  58. // Ensure SVG has correct XML declaration
  59. if (!svgGraph.includes('<?xml'))
  60. svgGraph = `<?xml version="1.0" encoding="UTF-8"?>${svgGraph}`
  61. const blob = new Blob([new TextEncoder().encode(svgGraph)], { type: 'image/svg+xml;charset=utf-8' })
  62. return new Promise((resolve, reject) => {
  63. const reader = new FileReader()
  64. reader.onloadend = () => resolve(reader.result as string)
  65. reader.onerror = reject
  66. reader.readAsDataURL(blob)
  67. })
  68. }
  69. catch (error) {
  70. console.error('Error converting SVG to base64:', error)
  71. return Promise.resolve('')
  72. }
  73. }
  74. /**
  75. * Processes SVG for theme styling
  76. */
  77. export function processSvgForTheme(
  78. svg: string,
  79. isDark: boolean,
  80. isHandDrawn: boolean,
  81. themes: {
  82. light: any
  83. dark: any
  84. },
  85. ): string {
  86. let processedSvg = svg
  87. if (isDark) {
  88. processedSvg = processedSvg
  89. .replace(/style="fill: ?#000000"/g, 'style="fill: #e2e8f0"')
  90. .replace(/style="stroke: ?#000000"/g, 'style="stroke: #94a3b8"')
  91. .replace(/<rect [^>]*fill="#ffffff"/g, '<rect $& fill="#1e293b"')
  92. if (isHandDrawn) {
  93. processedSvg = processedSvg
  94. .replace(/fill="#[a-fA-F0-9]{6}"/g, `fill="${themes.dark.nodeColors[0].bg}"`)
  95. .replace(/stroke="#[a-fA-F0-9]{6}"/g, `stroke="${themes.dark.connectionColor}"`)
  96. .replace(/stroke-width="1"/g, 'stroke-width="1.5"')
  97. }
  98. else {
  99. let i = 0
  100. themes.dark.nodeColors.forEach(() => {
  101. const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
  102. processedSvg = processedSvg.replace(regex, (match: string) => {
  103. const colorIndex = i % themes.dark.nodeColors.length
  104. i++
  105. return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.dark.nodeColors[colorIndex].bg}"`)
  106. })
  107. })
  108. processedSvg = processedSvg
  109. .replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
  110. `<path stroke="${themes.dark.connectionColor}" stroke-width="1.5"`)
  111. .replace(/<(line|polyline) [^>]*stroke="#[a-fA-F0-9]{6}"/g,
  112. `<$1 stroke="${themes.dark.connectionColor}" stroke-width="1.5"`)
  113. }
  114. }
  115. else {
  116. if (isHandDrawn) {
  117. processedSvg = processedSvg
  118. .replace(/fill="#[a-fA-F0-9]{6}"/g, `fill="${themes.light.nodeColors[0].bg}"`)
  119. .replace(/stroke="#[a-fA-F0-9]{6}"/g, `stroke="${themes.light.connectionColor}"`)
  120. .replace(/stroke-width="1"/g, 'stroke-width="1.5"')
  121. }
  122. else {
  123. themes.light.nodeColors.forEach(() => {
  124. const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
  125. let i = 0
  126. processedSvg = processedSvg.replace(regex, (match: string) => {
  127. const colorIndex = i % themes.light.nodeColors.length
  128. i++
  129. return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.light.nodeColors[colorIndex].bg}"`)
  130. })
  131. })
  132. processedSvg = processedSvg
  133. .replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
  134. `<path stroke="${themes.light.connectionColor}"`)
  135. .replace(/<(line|polyline) [^>]*stroke="#[a-fA-F0-9]{6}"/g,
  136. `<$1 stroke="${themes.light.connectionColor}"`)
  137. }
  138. }
  139. return processedSvg
  140. }
  141. /**
  142. * Checks if mermaid code is complete and valid
  143. */
  144. export function isMermaidCodeComplete(code: string): boolean {
  145. if (!code || code.trim().length === 0)
  146. return false
  147. try {
  148. const trimmedCode = code.trim()
  149. // Special handling for gantt charts
  150. if (trimmedCode.startsWith('gantt')) {
  151. // For gantt charts, check if it has at least a title and one task
  152. const lines = trimmedCode.split('\n').filter(line => line.trim().length > 0)
  153. return lines.length >= 3
  154. }
  155. // Check for basic syntax structure
  156. const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram)/.test(trimmedCode)
  157. // Check for balanced brackets and parentheses
  158. const isBalanced = (() => {
  159. const stack = []
  160. const pairs = { '{': '}', '[': ']', '(': ')' }
  161. for (const char of trimmedCode) {
  162. if (char in pairs) {
  163. stack.push(char)
  164. }
  165. else if (Object.values(pairs).includes(char)) {
  166. const last = stack.pop()
  167. if (pairs[last as keyof typeof pairs] !== char)
  168. return false
  169. }
  170. }
  171. return stack.length === 0
  172. })()
  173. // Check for common syntax errors
  174. const hasNoSyntaxErrors = !trimmedCode.includes('undefined')
  175. && !trimmedCode.includes('[object Object]')
  176. && trimmedCode.split('\n').every(line =>
  177. !(line.includes('-->') && !line.match(/\S+\s*-->\s*\S+/)))
  178. return hasValidStart && isBalanced && hasNoSyntaxErrors
  179. }
  180. catch (error) {
  181. console.debug('Mermaid code validation error:', error)
  182. return false
  183. }
  184. }
  185. /**
  186. * Helper to wait for DOM element with retry mechanism
  187. */
  188. export function waitForDOMElement(callback: () => Promise<any>, maxAttempts = 3, delay = 100): Promise<any> {
  189. return new Promise((resolve, reject) => {
  190. let attempts = 0
  191. const tryRender = async () => {
  192. try {
  193. resolve(await callback())
  194. }
  195. catch (error) {
  196. attempts++
  197. if (attempts < maxAttempts)
  198. setTimeout(tryRender, delay)
  199. else
  200. reject(error)
  201. }
  202. }
  203. tryRender()
  204. })
  205. }