Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

utils.ts 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. export function cleanUpSvgCode(svgCode: string): string {
  2. return svgCode.replaceAll('<br>', '<br/>')
  3. }
  4. /**
  5. * Prepares mermaid code for rendering by sanitizing common syntax issues.
  6. * @param {string} mermaidCode - The mermaid code to prepare
  7. * @param {'classic' | 'handDrawn'} style - The rendering style
  8. * @returns {string} - The prepared mermaid code
  9. */
  10. export const prepareMermaidCode = (mermaidCode: string, style: 'classic' | 'handDrawn'): string => {
  11. if (!mermaidCode || typeof mermaidCode !== 'string')
  12. return ''
  13. let code = mermaidCode.trim()
  14. // Security: Sanitize against javascript: protocol in click events (XSS vector)
  15. code = code.replace(/(\bclick\s+\w+\s+")javascript:[^"]*(")/g, '$1#$2')
  16. // Convenience: Basic BR replacement. This is a common and safe operation.
  17. code = code.replace(/<br\s*\/?>/g, '\n')
  18. let finalCode = code
  19. // Hand-drawn style requires some specific clean-up.
  20. if (style === 'handDrawn') {
  21. finalCode = finalCode
  22. .replace(/style\s+[^\n]+/g, '')
  23. .replace(/linkStyle\s+[^\n]+/g, '')
  24. .replace(/^flowchart/, 'graph')
  25. .replace(/class="[^"]*"/g, '')
  26. .replace(/fill="[^"]*"/g, '')
  27. .replace(/stroke="[^"]*"/g, '')
  28. // Ensure hand-drawn style charts always start with graph
  29. if (!finalCode.startsWith('graph') && !finalCode.startsWith('flowchart'))
  30. finalCode = `graph TD\n${finalCode}`
  31. }
  32. return finalCode
  33. }
  34. /**
  35. * Converts SVG to base64 string for image rendering
  36. */
  37. export function svgToBase64(svgGraph: string): Promise<string> {
  38. if (!svgGraph)
  39. return Promise.resolve('')
  40. try {
  41. // Ensure SVG has correct XML declaration
  42. if (!svgGraph.includes('<?xml'))
  43. svgGraph = `<?xml version="1.0" encoding="UTF-8"?>${svgGraph}`
  44. const blob = new Blob([new TextEncoder().encode(svgGraph)], { type: 'image/svg+xml;charset=utf-8' })
  45. return new Promise((resolve, reject) => {
  46. const reader = new FileReader()
  47. reader.onloadend = () => resolve(reader.result as string)
  48. reader.onerror = reject
  49. reader.readAsDataURL(blob)
  50. })
  51. }
  52. catch (error) {
  53. return Promise.resolve('')
  54. }
  55. }
  56. /**
  57. * Processes SVG for theme styling
  58. */
  59. export function processSvgForTheme(
  60. svg: string,
  61. isDark: boolean,
  62. isHandDrawn: boolean,
  63. themes: {
  64. light: any
  65. dark: any
  66. },
  67. ): string {
  68. let processedSvg = svg
  69. if (isDark) {
  70. processedSvg = processedSvg
  71. .replace(/style="fill: ?#000000"/g, 'style="fill: #e2e8f0"')
  72. .replace(/style="stroke: ?#000000"/g, 'style="stroke: #94a3b8"')
  73. .replace(/<rect [^>]*fill="#ffffff"/g, '<rect $& fill="#1e293b"')
  74. if (isHandDrawn) {
  75. processedSvg = processedSvg
  76. .replace(/fill="#[a-fA-F0-9]{6}"/g, `fill="${themes.dark.nodeColors[0].bg}"`)
  77. .replace(/stroke="#[a-fA-F0-9]{6}"/g, `stroke="${themes.dark.connectionColor}"`)
  78. .replace(/stroke-width="1"/g, 'stroke-width="1.5"')
  79. }
  80. else {
  81. let i = 0
  82. const nodeColorRegex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
  83. processedSvg = processedSvg.replace(nodeColorRegex, (match: string) => {
  84. const colorIndex = i % themes.dark.nodeColors.length
  85. i++
  86. return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.dark.nodeColors[colorIndex].bg}"`)
  87. })
  88. processedSvg = processedSvg
  89. .replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
  90. `<path stroke="${themes.dark.connectionColor}" stroke-width="1.5"`)
  91. .replace(/<(line|polyline) [^>]*stroke="#[a-fA-F0-9]{6}"/g,
  92. `<$1 stroke="${themes.dark.connectionColor}" stroke-width="1.5"`)
  93. }
  94. }
  95. else {
  96. if (isHandDrawn) {
  97. processedSvg = processedSvg
  98. .replace(/fill="#[a-fA-F0-9]{6}"/g, `fill="${themes.light.nodeColors[0].bg}"`)
  99. .replace(/stroke="#[a-fA-F0-9]{6}"/g, `stroke="${themes.light.connectionColor}"`)
  100. .replace(/stroke-width="1"/g, 'stroke-width="1.5"')
  101. }
  102. else {
  103. let i = 0
  104. const nodeColorRegex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g
  105. processedSvg = processedSvg.replace(nodeColorRegex, (match: string) => {
  106. const colorIndex = i % themes.light.nodeColors.length
  107. i++
  108. return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.light.nodeColors[colorIndex].bg}"`)
  109. })
  110. processedSvg = processedSvg
  111. .replace(/<path [^>]*stroke="#[a-fA-F0-9]{6}"/g,
  112. `<path stroke="${themes.light.connectionColor}"`)
  113. .replace(/<(line|polyline) [^>]*stroke="#[a-fA-F0-9]{6}"/g,
  114. `<$1 stroke="${themes.light.connectionColor}"`)
  115. }
  116. }
  117. return processedSvg
  118. }
  119. /**
  120. * Checks if mermaid code is complete and valid
  121. */
  122. export function isMermaidCodeComplete(code: string): boolean {
  123. if (!code || code.trim().length === 0)
  124. return false
  125. try {
  126. const trimmedCode = code.trim()
  127. // Special handling for gantt charts
  128. if (trimmedCode.startsWith('gantt')) {
  129. // For gantt charts, check if it has at least a title and one task
  130. const lines = trimmedCode.split('\n').filter(line => line.trim().length > 0)
  131. return lines.length >= 3
  132. }
  133. // Special handling for mindmaps
  134. if (trimmedCode.startsWith('mindmap')) {
  135. // For mindmaps, check if it has at least a root node
  136. const lines = trimmedCode.split('\n').filter(line => line.trim().length > 0)
  137. return lines.length >= 2
  138. }
  139. // Check for basic syntax structure
  140. const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram|mindmap)/.test(trimmedCode)
  141. // The balanced bracket check was too strict and produced false negatives for valid
  142. // mermaid syntax like the asymmetric shape `A>B]`. Relying on Mermaid's own
  143. // parser is more robust.
  144. const isBalanced = true
  145. // Check for common syntax errors
  146. const hasNoSyntaxErrors = !trimmedCode.includes('undefined')
  147. && !trimmedCode.includes('[object Object]')
  148. && trimmedCode.split('\n').every(line =>
  149. !(line.includes('-->') && !line.match(/\S+\s*-->\s*\S+/)))
  150. return hasValidStart && isBalanced && hasNoSyntaxErrors
  151. }
  152. catch (error) {
  153. console.error('Mermaid code validation error:', error)
  154. return false
  155. }
  156. }
  157. /**
  158. * Helper to wait for DOM element with retry mechanism
  159. */
  160. export function waitForDOMElement(callback: () => Promise<any>, maxAttempts = 3, delay = 100): Promise<any> {
  161. return new Promise((resolve, reject) => {
  162. let attempts = 0
  163. const tryRender = async () => {
  164. try {
  165. resolve(await callback())
  166. }
  167. catch (error) {
  168. attempts++
  169. if (attempts < maxAttempts)
  170. setTimeout(tryRender, delay)
  171. else
  172. reject(error)
  173. }
  174. }
  175. tryRender()
  176. })
  177. }