| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- 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')
- // Ensure graph has direction
- .replace(/^graph\s+((?:TB|BT|RL|LR)*)/, (match, direction) => {
- return direction ? match : 'graph TD'
- })
- // 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()
- })
- }
|