feat(new tool): Mermaid exporter
This commit is contained in:
		
							parent
							
								
									08d977b8cd
								
							
						
					
					
						commit
						6d868f9182
					
				
							
								
								
									
										11
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -127,22 +127,27 @@ declare module '@vue/runtime-core' { | ||||
|     MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default'] | ||||
|     MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default'] | ||||
|     MenuLayout: typeof import('./src/components/MenuLayout.vue')['default'] | ||||
|     MermaidExporter: typeof import('./src/tools/mermaid-exporter/mermaid-exporter.vue')['default'] | ||||
|     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] | ||||
|     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] | ||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||
|     NCheckbox: typeof import('naive-ui')['NCheckbox'] | ||||
|     NButton: typeof import('naive-ui')['NButton'] | ||||
|     NCode: typeof import('naive-ui')['NCode'] | ||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||
|     NDivider: typeof import('naive-ui')['NDivider'] | ||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||
|     NForm: typeof import('naive-ui')['NForm'] | ||||
|     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||
|     NH1: typeof import('naive-ui')['NH1'] | ||||
|     NH3: typeof import('naive-ui')['NH3'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NLayout: typeof import('naive-ui')['NLayout'] | ||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||
|     NMenu: typeof import('naive-ui')['NMenu'] | ||||
|     NSpace: typeof import('naive-ui')['NSpace'] | ||||
|     NTable: typeof import('naive-ui')['NTable'] | ||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||
|     NSlider: typeof import('naive-ui')['NSlider'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] | ||||
|     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] | ||||
|     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||
|  | ||||
| @ -78,6 +78,7 @@ | ||||
|     "markdown-it": "^14.0.0", | ||||
|     "marked": "^10.0.0", | ||||
|     "mathjs": "^11.9.1", | ||||
|     "mermaid": "^11.6.0", | ||||
|     "mime-types": "^2.1.35", | ||||
|     "monaco-editor": "^0.43.0", | ||||
|     "naive-ui": "^2.35.0", | ||||
|  | ||||
							
								
								
									
										1031
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1031
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,6 +1,7 @@ | ||||
| import { tool as base64FileConverter } from './base64-file-converter'; | ||||
| import { tool as base64StringConverter } from './base64-string-converter'; | ||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||
| import { tool as mermaidExporter } from './mermaid-exporter'; | ||||
| import { tool as emailNormalizer } from './email-normalizer'; | ||||
| 
 | ||||
| import { tool as asciiTextDrawer } from './ascii-text-drawer'; | ||||
| @ -116,6 +117,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|       xmlToJson, | ||||
|       jsonToXml, | ||||
|       markdownToHtml, | ||||
|       mermaidExporter, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|  | ||||
							
								
								
									
										12
									
								
								src/tools/mermaid-exporter/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/mermaid-exporter/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| import { Markdown } from '@vicons/tabler'; | ||||
| import { defineTool } from '../tool'; | ||||
| 
 | ||||
| export const tool = defineTool({ | ||||
|   name: 'Mermaid exporter', | ||||
|   path: '/mermaid-exporter', | ||||
|   description: 'Convert Markdown (Mermaid) to image and allow to export to PNG, JPG & SVG', | ||||
|   keywords: ['mermaid', 'exporter', 'markdown', 'MD'], | ||||
|   component: () => import('./mermaid-exporter.vue'), | ||||
|   icon: Markdown, | ||||
|   createdAt: new Date('2025-04-11'), | ||||
| }); | ||||
							
								
								
									
										15
									
								
								src/tools/mermaid-exporter/mermaid-exporter.e2e.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/tools/mermaid-exporter/mermaid-exporter.e2e.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| import { test, expect } from '@playwright/test'; | ||||
| 
 | ||||
| test.describe('Tool - Mermaid exporter', () => { | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('/mermaid-exporter'); | ||||
|   }); | ||||
| 
 | ||||
|   test('Has correct title', async ({ page }) => { | ||||
|     await expect(page).toHaveTitle('Mermaid exporter - IT Tools'); | ||||
|   }); | ||||
| 
 | ||||
|   test('', async ({ page }) => { | ||||
| 
 | ||||
|   }); | ||||
| }); | ||||
| @ -0,0 +1,7 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| 
 | ||||
| // import { } from './mermaid-exporter.service';
 | ||||
| //
 | ||||
| // describe('mermaid-exporter', () => {
 | ||||
| //
 | ||||
| // })
 | ||||
							
								
								
									
										177
									
								
								src/tools/mermaid-exporter/mermaid-exporter.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								src/tools/mermaid-exporter/mermaid-exporter.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,177 @@ | ||||
| <script setup lang="ts"> | ||||
| import { nextTick, onMounted, ref, watch } from 'vue'; | ||||
| import mermaid from 'mermaid'; | ||||
| 
 | ||||
| mermaid.initialize({ startOnLoad: false }); | ||||
| 
 | ||||
| const mermaidCode = ref<string>(`graph TD | ||||
| A[Start] --> B{Is it working?} | ||||
| B -- Yes --> C[Great!] | ||||
| B -- No --> D[Fix it!] | ||||
| `); | ||||
| 
 | ||||
| const mermaidContainer = ref<HTMLElement | null>(null); | ||||
| 
 | ||||
| async function renderMermaid(): Promise<void> { | ||||
|   if (mermaidContainer.value) { | ||||
|     mermaidContainer.value.innerHTML = ''; | ||||
|     try { | ||||
|       mermaid.parse(mermaidCode.value); | ||||
|       const { svg } = await mermaid.render('graphDiv', mermaidCode.value); | ||||
|       mermaidContainer.value.innerHTML = svg; | ||||
|     } | ||||
|     catch (error: unknown) { | ||||
|       mermaidContainer.value.innerHTML = '<p class="error">Invalid Mermaid syntax</p>'; | ||||
|       console.error('Mermaid error:', error); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| watch(mermaidCode, () => { | ||||
|   nextTick(() => { | ||||
|     renderMermaid(); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   renderMermaid(); | ||||
| }); | ||||
| 
 | ||||
| function fixSvgSize(svg: string): string { | ||||
|   const match = svg.match(/viewBox="([\d\s.-]+)"/); | ||||
|   if (!match) { | ||||
|     return svg; | ||||
|   } | ||||
| 
 | ||||
|   // eslint-disable-next-line unused-imports/no-unused-vars | ||||
|   const [minX, minY, width, height] = match[1].split(/\s+/).map(Number); | ||||
| 
 | ||||
|   svg = svg.replace(/width="[^"]*"/, `width="${width}"`); | ||||
|   svg = svg.replace(/height="[^"]*"/, `height="${height}"`); | ||||
| 
 | ||||
|   if (!/width="/.test(svg)) { | ||||
|     svg = svg.replace('<svg', `<svg width="${width}"`); | ||||
|   } | ||||
|   if (!/height="/.test(svg)) { | ||||
|     svg = svg.replace('<svg', `<svg height="${height}"`); | ||||
|   } | ||||
| 
 | ||||
|   return svg; | ||||
| } | ||||
| 
 | ||||
| function exportAs(format: 'svg' | 'png' | 'jpg'): void { | ||||
|   const container = mermaidContainer.value; | ||||
|   if (!container) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   const svgElement = container.querySelector('svg'); | ||||
|   if (!svgElement) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   let svgData = new XMLSerializer().serializeToString(svgElement); | ||||
| 
 | ||||
|   if (!svgData.includes('xmlns=')) { | ||||
|     svgData = svgData.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"'); | ||||
|   } | ||||
| 
 | ||||
|   svgData = fixSvgSize(svgData); | ||||
| 
 | ||||
|   if (format === 'svg') { | ||||
|     const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); | ||||
|     const url = URL.createObjectURL(blob); | ||||
|     const link = document.createElement('a'); | ||||
|     link.href = url; | ||||
|     link.download = 'diagram.svg'; | ||||
|     link.click(); | ||||
|     URL.revokeObjectURL(url); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   const blob = new Blob([svgData], { type: 'image/svg+xml' }); | ||||
|   const reader = new FileReader(); | ||||
|   const scaleFactor = 3; | ||||
| 
 | ||||
|   reader.onloadend = () => { | ||||
|     const base64data = reader.result as string; | ||||
|     const image = new Image(); | ||||
| 
 | ||||
|     image.onload = () => { | ||||
|       const canvas = document.createElement('canvas'); | ||||
|       canvas.width = image.width * scaleFactor; | ||||
|       canvas.height = image.height * scaleFactor; | ||||
| 
 | ||||
|       const ctx = canvas.getContext('2d'); | ||||
|       if (!ctx) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       ctx.setTransform(scaleFactor, 0, 0, scaleFactor, 0, 0); | ||||
|       ctx.drawImage(image, 0, 0); | ||||
| 
 | ||||
|       const mime = format === 'png' ? 'image/png' : 'image/jpeg'; | ||||
|       const link = document.createElement('a'); | ||||
|       link.download = `diagram.${format}`; | ||||
|       link.href = canvas.toDataURL(mime); | ||||
|       link.click(); | ||||
|     }; | ||||
| 
 | ||||
|     image.src = base64data; | ||||
|   }; | ||||
| 
 | ||||
|   reader.readAsDataURL(blob); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div> | ||||
|     <c-input-text | ||||
|       v-model:value="mermaidCode" | ||||
|       class="" | ||||
|       multiline raw-text | ||||
|       placeholder="Write your Mermaid code here..." | ||||
|       rows="8" | ||||
|       autofocus | ||||
|       label="Your Mermaid to convert:" | ||||
|     /> | ||||
|     <n-divider /> | ||||
|     <div flex justify-center class="diagram-container"> | ||||
|       <div ref="mermaidContainer" /> | ||||
|     </div> | ||||
| 
 | ||||
|     <div flex justify-center class="buttons"> | ||||
|       <n-button @click="exportAs('png')"> | ||||
|         Export as PNG | ||||
|       </n-button> | ||||
|       <n-button @click="exportAs('jpg')"> | ||||
|         Export as JPG | ||||
|       </n-button> | ||||
|       <n-button @click="exportAs('svg')"> | ||||
|         Export as SVG | ||||
|       </n-button> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style lang="less" scoped> | ||||
| .diagram-container { | ||||
|     border: 1px solid var(--theme-default-color); | ||||
|     padding: 15px; | ||||
|     border-radius: 6px; | ||||
|     background-color: var(--theme-default-color); | ||||
|     overflow-x: auto; | ||||
|     margin-bottom: 20px; | ||||
| 
 | ||||
|     .error { | ||||
|       color: var(--theme-error-color); | ||||
|       font-weight: bold; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .buttons { | ||||
|     display: flex; | ||||
|     gap: 10px; | ||||
|     flex-wrap: wrap; | ||||
|   } | ||||
| </style> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user