mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-20 14:56:17 -04:00
Merge 08d76f3a83
into 07eea0f484
This commit is contained in:
commit
620d8ba609
8 changed files with 1111 additions and 162 deletions
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'),
|
||||||
|
});
|
39
src/tools/mermaid-exporter/mermaid-exporter.e2e.spec.ts
Normal file
39
src/tools/mermaid-exporter/mermaid-exporter.e2e.spec.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Mermaid Diagram Renderer', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/mermaid-exporter');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render the initial diagram', async ({ page }) => {
|
||||||
|
const diagramContainer = page.locator('.diagram-container svg');
|
||||||
|
await expect(diagramContainer).toBeVisible();
|
||||||
|
await expect(diagramContainer).toHaveAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update diagram when user edits Mermaid code', async ({ page }) => {
|
||||||
|
const input = page.getByLabel('Your Mermaid to convert:');
|
||||||
|
const updatedMermaid = `
|
||||||
|
graph LR
|
||||||
|
X[Start] --> Y{Working?}
|
||||||
|
Y -- Yes --> Z[Success!]
|
||||||
|
Y -- No --> W[Fix again!]
|
||||||
|
`;
|
||||||
|
await input.fill(updatedMermaid);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
const svg = page.locator('.diagram-container svg');
|
||||||
|
await expect(svg).toContainText('Start');
|
||||||
|
await expect(svg).toContainText('Success!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow exporting in SVG, PNG, and JPG', async ({ page }) => {
|
||||||
|
const formats = ['svg', 'png', 'jpg'];
|
||||||
|
|
||||||
|
for (const format of formats) {
|
||||||
|
const button = page.getByRole('button', { name: new RegExp(`Export as ${format}`, 'i') });
|
||||||
|
await expect(button).toBeVisible();
|
||||||
|
await button.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
0
src/tools/mermaid-exporter/mermaid-exporter.service.ts
Normal file
0
src/tools/mermaid-exporter/mermaid-exporter.service.ts
Normal file
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…
Add table
Add a link
Reference in a new issue