您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

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