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'] |     MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default'] | ||||||
|     MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default'] |     MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default'] | ||||||
|     MenuLayout: typeof import('./src/components/MenuLayout.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'] |     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] | ||||||
|     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] |     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] | ||||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.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'] |     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] |     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||||
|     NDivider: typeof import('naive-ui')['NDivider'] |     NDivider: typeof import('naive-ui')['NDivider'] | ||||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] |     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||||
|  |     NForm: typeof import('naive-ui')['NForm'] | ||||||
|  |     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||||
|     NH1: typeof import('naive-ui')['NH1'] |     NH1: typeof import('naive-ui')['NH1'] | ||||||
|     NH3: typeof import('naive-ui')['NH3'] |     NH3: typeof import('naive-ui')['NH3'] | ||||||
|     NIcon: typeof import('naive-ui')['NIcon'] |     NIcon: typeof import('naive-ui')['NIcon'] | ||||||
|     NLayout: typeof import('naive-ui')['NLayout'] |     NLayout: typeof import('naive-ui')['NLayout'] | ||||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] |     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||||
|     NMenu: typeof import('naive-ui')['NMenu'] |     NMenu: typeof import('naive-ui')['NMenu'] | ||||||
|     NSpace: typeof import('naive-ui')['NSpace'] |     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||||
|     NTable: typeof import('naive-ui')['NTable'] |     NSlider: typeof import('naive-ui')['NSlider'] | ||||||
|  |     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||||
|     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] |     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'] |     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'] |     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||||
|  | |||||||
| @ -78,6 +78,7 @@ | |||||||
|     "markdown-it": "^14.0.0", |     "markdown-it": "^14.0.0", | ||||||
|     "marked": "^10.0.0", |     "marked": "^10.0.0", | ||||||
|     "mathjs": "^11.9.1", |     "mathjs": "^11.9.1", | ||||||
|  |     "mermaid": "^11.6.0", | ||||||
|     "mime-types": "^2.1.35", |     "mime-types": "^2.1.35", | ||||||
|     "monaco-editor": "^0.43.0", |     "monaco-editor": "^0.43.0", | ||||||
|     "naive-ui": "^2.35.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 base64FileConverter } from './base64-file-converter'; | ||||||
| import { tool as base64StringConverter } from './base64-string-converter'; | import { tool as base64StringConverter } from './base64-string-converter'; | ||||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | 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 emailNormalizer } from './email-normalizer'; | ||||||
| 
 | 
 | ||||||
| import { tool as asciiTextDrawer } from './ascii-text-drawer'; | import { tool as asciiTextDrawer } from './ascii-text-drawer'; | ||||||
| @ -116,6 +117,7 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|       xmlToJson, |       xmlToJson, | ||||||
|       jsonToXml, |       jsonToXml, | ||||||
|       markdownToHtml, |       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