From f530489838ee982f8e662991546c8f0fddf9ad51 Mon Sep 17 00:00:00 2001 From: ssdwgg <1982549567@qq.com> Date: Sat, 30 May 2026 11:55:33 +0800 Subject: [PATCH 1/2] feat: add markdown table generator --- components.d.ts | 4 + src/tools/index.ts | 2 + src/tools/markdown-table-generator/index.ts | 12 ++ .../markdown-table-generator.service.test.ts | 50 +++++++ .../markdown-table-generator.service.ts | 65 +++++++++ .../markdown-table-generator.vue | 129 ++++++++++++++++++ 6 files changed, 262 insertions(+) create mode 100644 src/tools/markdown-table-generator/index.ts create mode 100644 src/tools/markdown-table-generator/markdown-table-generator.service.test.ts create mode 100644 src/tools/markdown-table-generator/markdown-table-generator.service.ts create mode 100644 src/tools/markdown-table-generator/markdown-table-generator.vue diff --git a/components.d.ts b/components.d.ts index 3e65c3cc52..100afb9e13 100644 --- a/components.d.ts +++ b/components.d.ts @@ -95,6 +95,7 @@ declare module '@vue/runtime-core' { IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] IconMdiClose: typeof import('~icons/mdi/close')['default'] IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] + IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] IconMdiEye: typeof import('~icons/mdi/eye')['default'] IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] IconMdiHeart: typeof import('~icons/mdi/heart')['default'] @@ -121,6 +122,7 @@ declare module '@vue/runtime-core' { LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default'] MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default'] MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default'] + MarkdownTableGenerator: typeof import('./src/tools/markdown-table-generator/markdown-table-generator.vue')['default'] MarkdownToHtml: typeof import('./src/tools/markdown-to-html/markdown-to-html.vue')['default'] MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default'] MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default'] @@ -135,12 +137,14 @@ declare module '@vue/runtime-core' { NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] + 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'] + NScrollbar: typeof import('naive-ui')['NScrollbar'] NSpace: typeof import('naive-ui')['NSpace'] NTable: typeof import('naive-ui')['NTable'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] diff --git a/src/tools/index.ts b/src/tools/index.ts index 388cfaf494..c1b1d47a3a 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -12,6 +12,7 @@ import { tool as jsonToXml } from './json-to-xml'; import { tool as regexTester } from './regex-tester'; import { tool as regexMemo } from './regex-memo'; import { tool as markdownToHtml } from './markdown-to-html'; +import { tool as markdownTableGenerator } from './markdown-table-generator'; import { tool as pdfSignatureChecker } from './pdf-signature-checker'; import { tool as numeronymGenerator } from './numeronym-generator'; import { tool as macAddressGenerator } from './mac-address-generator'; @@ -152,6 +153,7 @@ export const toolsByCategory: ToolCategory[] = [ jsonViewer, jsonMinify, jsonToCsv, + markdownTableGenerator, sqlPrettify, chmodCalculator, dockerRunToDockerComposeConverter, diff --git a/src/tools/markdown-table-generator/index.ts b/src/tools/markdown-table-generator/index.ts new file mode 100644 index 0000000000..faa6b76edb --- /dev/null +++ b/src/tools/markdown-table-generator/index.ts @@ -0,0 +1,12 @@ +import { Table } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Markdown Table Generator', + path: '/markdown-table-generator', + description: 'Create GitHub-flavored Markdown tables with a visual editor.', + keywords: ['markdown', 'table', 'generator', 'gfm', 'github'], + component: () => import('./markdown-table-generator.vue'), + icon: Table, + createdAt: new Date('2026-05-30'), +}); diff --git a/src/tools/markdown-table-generator/markdown-table-generator.service.test.ts b/src/tools/markdown-table-generator/markdown-table-generator.service.test.ts new file mode 100644 index 0000000000..e90884a11b --- /dev/null +++ b/src/tools/markdown-table-generator/markdown-table-generator.service.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { createMarkdownTable, escapeMarkdownTableCell, generateMarkdownTable } from './markdown-table-generator.service'; + +describe('markdown-table-generator service', () => { + describe('createMarkdownTable', () => { + it('creates a table with default headers and empty rows', () => { + expect(createMarkdownTable({ rows: 1, columns: 2 })).toEqual({ + headers: ['Column 1', 'Column 2'], + alignments: ['left', 'left'], + rows: [['', '']], + }); + }); + }); + + describe('escapeMarkdownTableCell', () => { + it('escapes pipes and converts line breaks to br tags', () => { + expect(escapeMarkdownTableCell(' foo | bar\nbaz ')).toBe('foo \\| barbaz'); + }); + }); + + describe('generateMarkdownTable', () => { + it('generates a markdown table with column alignment', () => { + expect(generateMarkdownTable({ + headers: ['Name', 'Count', 'Notes'], + alignments: ['left', 'right', 'center'], + rows: [ + ['Alpha', '10', 'Ready'], + ['Beta', '3', 'Needs review'], + ], + })).toMatchInlineSnapshot(` + "| Name | Count | Notes | + | :--- | ---: | :---: | + | Alpha | 10 | Ready | + | Beta | 3 | Needs review |" + `); + }); + + it('pads uneven rows to keep the table shape valid', () => { + expect(generateMarkdownTable({ + headers: ['Name'], + alignments: ['left'], + rows: [['Alpha', 'Extra']], + })).toMatchInlineSnapshot(` + "| Name | | + | :--- | :--- | + | Alpha | Extra |" + `); + }); + }); +}); diff --git a/src/tools/markdown-table-generator/markdown-table-generator.service.ts b/src/tools/markdown-table-generator/markdown-table-generator.service.ts new file mode 100644 index 0000000000..40c52f8883 --- /dev/null +++ b/src/tools/markdown-table-generator/markdown-table-generator.service.ts @@ -0,0 +1,65 @@ +export type MarkdownTableAlignment = 'left' | 'center' | 'right'; + +export interface MarkdownTable { + headers: string[] + alignments: MarkdownTableAlignment[] + rows: string[][] +} + +const alignmentSeparators: Record = { + left: ':---', + center: ':---:', + right: '---:', +}; + +export function createMarkdownTable({ rows = 2, columns = 3 }: { rows?: number; columns?: number } = {}): MarkdownTable { + return { + headers: Array.from({ length: columns }, (_, index) => `Column ${index + 1}`), + alignments: Array.from({ length: columns }, () => 'left'), + rows: Array.from({ length: rows }, () => Array.from({ length: columns }, () => '')), + }; +} + +export function escapeMarkdownTableCell(value: string): string { + return value + .replace(/\|/g, '\\|') + .replace(/\r\n|\r|\n/g, '') + .trim(); +} + +function getColumnCount(table: MarkdownTable): number { + return Math.max( + table.headers.length, + table.alignments.length, + ...table.rows.map(row => row.length), + ); +} + +function getCells(cells: string[], columns: number): string[] { + return Array.from({ length: columns }, (_, index) => escapeMarkdownTableCell(cells[index] ?? '')); +} + +function formatRow(cells: string[]): string { + return `| ${cells.join(' | ')} |`; +} + +export function generateMarkdownTable(table: MarkdownTable): string { + const columns = getColumnCount(table); + + if (columns === 0) { + return ''; + } + + const headers = getCells(table.headers, columns); + const separators = Array.from( + { length: columns }, + (_, index) => alignmentSeparators[table.alignments[index] ?? 'left'], + ); + const rows = table.rows.map(row => formatRow(getCells(row, columns))); + + return [ + formatRow(headers), + formatRow(separators), + ...rows, + ].join('\n'); +} diff --git a/src/tools/markdown-table-generator/markdown-table-generator.vue b/src/tools/markdown-table-generator/markdown-table-generator.vue new file mode 100644 index 0000000000..f091c3417e --- /dev/null +++ b/src/tools/markdown-table-generator/markdown-table-generator.vue @@ -0,0 +1,129 @@ + + + + + + + + Add row + + + Add column + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f556b5ed8dabb3c6a03b577cca4c8f66a4a699e8 Mon Sep 17 00:00:00 2001 From: ssdwgg <1982549567@qq.com> Date: Sat, 30 May 2026 16:38:46 +0800 Subject: [PATCH 2/2] fix: add scopes to markdown table headers --- .../markdown-table-generator/markdown-table-generator.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/markdown-table-generator/markdown-table-generator.vue b/src/tools/markdown-table-generator/markdown-table-generator.vue index f091c3417e..9468f55a22 100644 --- a/src/tools/markdown-table-generator/markdown-table-generator.vue +++ b/src/tools/markdown-table-generator/markdown-table-generator.vue @@ -61,7 +61,7 @@ function removeRow(index: number) { - + - +