From 1b3c602764c37e6cbae13c12479bbdfe5a888753 Mon Sep 17 00:00:00 2001 From: persist-1 Date: Wed, 20 Aug 2025 18:26:12 +0800 Subject: [PATCH 01/14] feat: Export the preview image as svg - new DOM:Export the preview image as svg(With corresponding translation) --- package.json | 1 + pnpm-lock.yaml | 80 ++++-- public/locales/cn/translation.json | 5 + public/locales/en/translation.json | 7 + src/modules/export/dropdown.tsx | 53 ++++ src/modules/export/index.ts | 5 + src/modules/export/utils.ts | 376 +++++++++++++++++++++++++++++ src/modules/home/index.tsx | 56 ++++- 8 files changed, 560 insertions(+), 23 deletions(-) create mode 100644 public/locales/en/translation.json create mode 100644 src/modules/export/dropdown.tsx create mode 100644 src/modules/export/index.ts create mode 100644 src/modules/export/utils.ts diff --git a/package.json b/package.json index ea994fe..7d9dc1f 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", + "@types/html2canvas": "^1.0.0", "@types/node": "^18.0.0", "@types/react": "^18.0.11", "@types/react-dom": "^18.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 147c31c..82a1bf6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,10 +97,10 @@ importers: version: 2.4.0 tailwindcss: specifier: ^3.4.4 - version: 3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)) + version: 3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)) tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3))) + version: 1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3))) typescript: specifier: ^5.5.0 version: 5.5.3 @@ -122,7 +122,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^2.22.2 - version: 2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)) + version: 2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -132,6 +132,9 @@ importers: '@testing-library/react': specifier: ^16.0.0 version: 16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/html2canvas': + specifier: ^1.0.0 + version: 1.0.0 '@types/node': specifier: ^18.0.0 version: 18.19.39 @@ -176,7 +179,7 @@ importers: version: 2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1) vitest-canvas-mock: specifier: ^0.3.3 - version: 0.3.3(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)) + version: 0.3.3(vitest@2.0.5) packages: @@ -464,6 +467,7 @@ packages: '@eslint-react/tools@1.5.25': resolution: {integrity: sha512-i036JyZesrpBcLzREFP9Cknuq4y3FXGZhvm7eswVhI19/SiMnn6+XtMc9hQdVYqlSTwXSauRWeLaQJTEyrl2SQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@eslint-react/types@1.5.25': resolution: {integrity: sha512-MqlyjJyjiR+njDUeizcjnRL1Ar6wG5jz58o8JJuwnsLaU+m6ttIih7c+KhEN8pAEUofex7sfqdcejYO6yO+cMQ==} @@ -1236,6 +1240,10 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/html2canvas@1.0.0': + resolution: {integrity: sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==} + deprecated: This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed. + '@types/js-cookie@2.2.7': resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} @@ -1540,6 +1548,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1690,6 +1702,9 @@ packages: css-in-js-utils@3.1.0: resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-tree@1.1.3: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} @@ -2260,6 +2275,10 @@ packages: html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3191,6 +3210,9 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -3377,6 +3399,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -3597,7 +3622,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1))': + '@antfu/eslint-config@2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5)': dependencies: '@antfu/install-pkg': 0.3.3 '@clack/prompts': 0.7.0 @@ -3622,7 +3647,7 @@ snapshots: eslint-plugin-toml: 0.11.1(eslint@9.7.0) eslint-plugin-unicorn: 54.0.0(eslint@9.7.0) eslint-plugin-unused-imports: 4.0.0(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0) - eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)) + eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5) eslint-plugin-vue: 9.27.0(eslint@9.7.0) eslint-plugin-yml: 1.14.0(eslint@9.7.0) eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.31)(eslint@9.7.0) @@ -4662,6 +4687,10 @@ snapshots: '@types/estree@1.0.5': {} + '@types/html2canvas@1.0.0': + dependencies: + html2canvas: 1.4.1 + '@types/js-cookie@2.2.7': {} '@types/json-schema@7.0.15': {} @@ -5038,6 +5067,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + binary-extensions@2.3.0: {} boolbase@1.0.0: {} @@ -5189,6 +5220,10 @@ snapshots: dependencies: hyphenate-style-name: 1.1.0 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-tree@1.1.3: dependencies: mdn-data: 2.0.14 @@ -5596,7 +5631,7 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3) - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)): + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5): dependencies: '@typescript-eslint/utils': 7.16.0(eslint@9.7.0)(typescript@5.5.3) eslint: 9.7.0 @@ -5892,6 +5927,11 @@ snapshots: dependencies: void-elements: 3.1.0 + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 @@ -6397,13 +6437,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.39 - postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)): + postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)): dependencies: lilconfig: 3.1.2 yaml: 2.4.5 optionalDependencies: postcss: 8.4.39 - ts-node: 10.9.2(@types/node@18.19.39)(typescript@5.5.3) + ts-node: 10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3) postcss-nested@6.0.1(postcss@8.4.39): dependencies: @@ -6785,11 +6825,11 @@ snapshots: tailwind-merge@2.4.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3))): + tailwindcss-animate@1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3))): dependencies: - tailwindcss: 3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)) + tailwindcss: 3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)) - tailwindcss@3.4.4(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)): + tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -6808,7 +6848,7 @@ snapshots: postcss: 8.4.39 postcss-import: 15.1.0(postcss@8.4.39) postcss-js: 4.0.1(postcss@8.4.39) - postcss-load-config: 4.0.2(postcss@8.4.39)(ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3)) + postcss-load-config: 4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)) postcss-nested: 6.0.1(postcss@8.4.39) postcss-selector-parser: 6.1.1 resolve: 1.22.8 @@ -6818,6 +6858,10 @@ snapshots: tapable@2.2.1: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + text-table@0.2.0: {} thenify-all@1.6.0: @@ -6879,7 +6923,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@18.19.39)(typescript@5.5.3): + ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -6896,6 +6940,8 @@ snapshots: typescript: 5.5.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.6.13(@swc/helpers@0.5.12) optional: true tsconfck@3.1.1(typescript@5.5.3): @@ -6967,6 +7013,10 @@ snapshots: util-deprecate@1.0.2: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + v8-compile-cache-lib@3.0.1: optional: true @@ -7020,7 +7070,7 @@ snapshots: '@types/node': 18.19.39 fsevents: 2.3.3 - vitest-canvas-mock@0.3.3(vitest@2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1)): + vitest-canvas-mock@0.3.3(vitest@2.0.5): dependencies: jest-canvas-mock: 2.5.2 vitest: 2.0.5(@types/node@18.19.39)(@vitest/ui@2.0.5)(happy-dom@14.12.3)(jsdom@24.1.1) diff --git a/public/locales/cn/translation.json b/public/locales/cn/translation.json index d2406f1..3a3f69f 100644 --- a/public/locales/cn/translation.json +++ b/public/locales/cn/translation.json @@ -48,6 +48,11 @@ "Parallel": "插入或", "After": "向后插入", "show more": "显示更多", + "Export": "导出", + "Export as SVG": "导出为SVG", + "No graph to export": "没有可导出的图形", + "Export failed": "导出失败", + "Graph exported as SVG": "图形已导出为SVG", "show less": "显示常用", "Expression": "表达式", "Content": "内容", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 0000000..72ce710 --- /dev/null +++ b/public/locales/en/translation.json @@ -0,0 +1,7 @@ +{ + "Export": "Export", + "Export as SVG": "Export as SVG", + "No graph to export": "No graph to export", + "Export failed": "Export failed", + "Graph exported as SVG": "Graph exported as SVG" +} \ No newline at end of file diff --git a/src/modules/export/dropdown.tsx b/src/modules/export/dropdown.tsx new file mode 100644 index 0000000..f1a78d0 --- /dev/null +++ b/src/modules/export/dropdown.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { DownloadIcon } from '@radix-ui/react-icons' +import { useTranslation } from 'react-i18next' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' + +export type ExportFormat = 'svg' + +interface ExportDropdownProps { + onExport: (format: ExportFormat) => void + disabled?: boolean + className?: string +} + +const ExportDropdown: React.FC = ({ + onExport, + disabled = false, + className, +}) => { + const { t } = useTranslation() + + const handleExport = (format: ExportFormat) => { + onExport(format) + } + + return ( + + + + + + handleExport('svg')}> + {t('Export as SVG')} + + + + ) +} + +export default ExportDropdown \ No newline at end of file diff --git a/src/modules/export/index.ts b/src/modules/export/index.ts new file mode 100644 index 0000000..5aacf82 --- /dev/null +++ b/src/modules/export/index.ts @@ -0,0 +1,5 @@ +// 导出功能模块统一入口 +export { default as ExportDropdown } from './dropdown' +export type { ExportFormat } from './dropdown' +export { exportGraph, exportSVG } from './utils' +export type { ExportFormat as UtilsExportFormat } from './utils' \ No newline at end of file diff --git a/src/modules/export/utils.ts b/src/modules/export/utils.ts new file mode 100644 index 0000000..d0d219a --- /dev/null +++ b/src/modules/export/utils.ts @@ -0,0 +1,376 @@ +export type ExportFormat = 'svg' + +/** + * 导出SVG格式 + * @param svgElement SVG元素 + * @param filename 文件名 + */ +export const exportSVG = (svgElement: SVGElement, filename: string = 'regex-graph') => { + try { + console.log('=== 开始SVG导出处理 ===') + console.log('原始SVG元素:', svgElement) + + // 克隆SVG元素以避免修改原始元素 + const clonedSvg = svgElement.cloneNode(true) as SVGElement + + // 设置SVG的xmlns属性以确保独立性 + clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg') + clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink') + + // 处理foreignObject元素,将其转换为原生SVG文本元素 + const foreignObjects = clonedSvg.querySelectorAll('foreignObject') + console.log(`找到 ${foreignObjects.length} 个 foreignObject 元素`) + foreignObjects.forEach((fo, index) => { + console.log(`处理 foreignObject ${index + 1}:`, fo) + const div = fo.querySelector('div') + if (div) { + // 智能提取文本内容,包括处理图标元素 + console.log('div内容:', div.innerHTML) + const extractedContent = extractTextWithIcons(div) + console.log('提取的内容:', extractedContent) + const x = parseFloat(fo.getAttribute('x') || '0') + const y = parseFloat(fo.getAttribute('y') || '0') + const width = parseFloat(fo.getAttribute('width') || '0') + const height = parseFloat(fo.getAttribute('height') || '0') + + // 创建SVG text元素替换foreignObject + const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text') + textElement.setAttribute('x', (x + width / 2).toString()) + textElement.setAttribute('y', (y + height / 2 + 6).toString()) // 调整垂直居中 + textElement.setAttribute('text-anchor', 'middle') + textElement.setAttribute('dominant-baseline', 'middle') + textElement.setAttribute('font-family', 'ui-monospace, monospace') + textElement.setAttribute('font-size', fo.getAttribute('font-size') || '16') + textElement.setAttribute('fill', '#111827') + // 确保特殊Unicode字符(如∞)能正确显示 + textElement.setAttribute('unicode-bidi', 'embed') + textElement.setAttribute('direction', 'ltr') + textElement.textContent = extractedContent + + // 替换foreignObject + fo.parentNode?.replaceChild(textElement, fo) + } + }) + + // 处理主SVG中的独立图标元素(如循环图标) + const allSvgs = clonedSvg.querySelectorAll('svg') + console.log(`导出处理:找到 ${allSvgs.length} 个SVG元素`) + + allSvgs.forEach((iconSvg, index) => { + // 跳过主SVG容器本身 + if (iconSvg === clonedSvg) { + console.log(`跳过主SVG容器 (索引 ${index})`) + return + } + + const paths = iconSvg.querySelectorAll('path') + let isLoopIcon = false + + console.log(`检查SVG ${index},包含 ${paths.length} 个path元素`) + + // 检测循环图标 + for (let i = 0; i < paths.length; i++) { + const path = paths[i] + const d = path.getAttribute('d') || '' + console.log(`Path ${i}: ${d.substring(0, 50)}...`) + + if ((d.includes('M17 1l4 4-4 4') || d.includes('M7 23l-4-4 4-4')) || + (d.includes('M3 11V9a4 4') && d.includes('M21 13v2a4 4')) || + (d.includes('l4 4-4 4') && d.includes('l-4-4 4-4'))) { + isLoopIcon = true + console.log(`检测到循环图标路径: ${d}`) + break + } + } + + if (isLoopIcon) { + console.log('在主SVG中检测到循环图标,正在转换为文本') + + // 获取图标的位置和尺寸信息 + const width = parseFloat(iconSvg.getAttribute('width') || '18') + const height = parseFloat(iconSvg.getAttribute('height') || '18') + const transform = iconSvg.getAttribute('transform') || '' + + console.log(`图标尺寸: ${width}x${height}, transform: ${transform}`) + + // 尝试从父元素的样式或属性获取位置 + let x = 0, y = 0 + + // 检查transform属性 + const translateMatch = transform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/) + if (translateMatch) { + x = parseFloat(translateMatch[1]) + y = parseFloat(translateMatch[2]) + console.log(`从transform获取位置: (${x}, ${y})`) + } else { + // 尝试从父元素获取位置信息 + const parent = iconSvg.parentElement + if (parent) { + const style = window.getComputedStyle(parent) + const left = parseFloat(style.left || '0') + const top = parseFloat(style.top || '0') + if (left || top) { + x = left + y = top + console.log(`从父元素样式获取位置: (${x}, ${y})`) + } + } + } + + // 创建文本元素替换图标 + const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text') + textElement.setAttribute('x', (x + width / 2).toString()) + textElement.setAttribute('y', (y + height / 2 + 4).toString()) + textElement.setAttribute('text-anchor', 'middle') + textElement.setAttribute('dominant-baseline', 'middle') + textElement.setAttribute('font-family', 'ui-monospace, monospace') + textElement.setAttribute('font-size', Math.min(width * 0.8, 14).toString()) + textElement.setAttribute('fill', 'currentColor') + textElement.textContent = '↻' + + console.log(`创建循环文本元素,位置: (${x + width / 2}, ${y + height / 2 + 4})`) + + // 替换图标SVG + if (iconSvg.parentNode) { + iconSvg.parentNode.replaceChild(textElement, iconSvg) + console.log('成功替换循环图标为文本') + } + } + }) + + // 获取计算样式并内联到SVG中 + const styleElement = document.createElementNS('http://www.w3.org/2000/svg', 'style') + const computedStyles = getComputedStylesForSVG(svgElement) + styleElement.textContent = computedStyles + clonedSvg.insertBefore(styleElement, clonedSvg.firstChild) + + // 序列化SVG + const serializer = new XMLSerializer() + const svgString = serializer.serializeToString(clonedSvg) + + // 创建Blob并下载 + const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }) + downloadBlob(blob, `${filename}.svg`) + } catch (error) { + console.error('SVG导出失败:', error) + throw new Error('SVG导出失败') + } +} + + + + + +/** + * 智能提取文本内容,包括处理图标元素 + * @param element - 要提取文本的DOM元素 + * @returns 提取的文本内容 + */ +function extractTextWithIcons(element: Element): string { + console.log('=== extractTextWithIcons 开始处理 ===') + console.log('处理元素:', element) + console.log('子节点数量:', element.childNodes.length) + + let result = '' + + // 遍历所有子节点 + for (let i = 0; i < element.childNodes.length; i++) { + const node = element.childNodes[i] + console.log(`处理子节点 ${i}:`, node.nodeType, node) + + if (node.nodeType === Node.TEXT_NODE) { + // 文本节点直接添加 + const textContent = node.textContent || '' + console.log('文本节点内容:', textContent) + result += textContent + } else if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as Element + console.log('元素节点标签:', el.tagName) + + // 检查是否是SVG图标(无穷符号或循环图标) + if (el.tagName.toLowerCase() === 'svg' && el.querySelector('path')) { + // 检查SVG是否包含特定图标的路径特征 + const paths = el.querySelectorAll('path') + let isInfinityIcon = false + let isLoopIcon = false + + for (let j = 0; j < paths.length; j++) { + const path = paths[j] + const d = path.getAttribute('d') || '' + + // 检测无穷符号:检查特定的路径模式 + // 无穷符号通常包含弧形路径和特定的坐标模式 + if ((d.includes('M248,128') && d.includes('95.6,39.6')) || + (d.includes('C') && d.includes('a56,56') && d.length > 100) || + (d.includes('a40,40') && d.includes('56.9') && d.length > 50)) { + isInfinityIcon = true + break + } + + // 检测循环图标:检查循环箭头的特定路径模式 + // 循环图标包含箭头路径和弧形连接线 + if ((d.includes('M17 1l4 4-4 4') || d.includes('M7 23l-4-4 4-4')) || + (d.includes('M3 11V9a4 4') && d.includes('M21 13v2a4 4')) || + (d.includes('l4 4-4 4') && d.includes('l-4-4 4-4'))) { + isLoopIcon = true + break + } + } + + if (isInfinityIcon) { + console.log('检测到无穷符号图标') + result += '∞' + } else if (isLoopIcon) { + console.log('检测到循环图标') + result += '↻' // 使用循环符号 + } else { + console.log('未识别的SVG图标,路径:', paths.length > 0 ? paths[0].getAttribute('d') : 'no paths') + // 其他SVG图标,尝试从aria-label或title获取文本 + const ariaLabel = el.getAttribute('aria-label') + const title = el.querySelector('title')?.textContent + if (ariaLabel) { + result += ariaLabel + } else if (title) { + result += title + } + } + } else { + // 递归处理其他元素 + result += extractTextWithIcons(el) + } + } + } + + return result +} + +/** + * 获取当前主题的颜色值 + * @returns 主题颜色对象 + */ +function getCurrentThemeColors() { + const rootStyles = getComputedStyle(document.documentElement) + + // 获取CSS变量值 + const graphColor = rootStyles.getPropertyValue('--graph').trim() || '#000000' + const foregroundColor = rootStyles.getPropertyValue('--foreground').trim() + const backgroundColor = rootStyles.getPropertyValue('--background').trim() + + // 转换HSL到十六进制(如果需要) + const convertHslToHex = (hsl: string): string => { + if (hsl.startsWith('#')) return hsl + if (!hsl) return '#000000' + + // 简单的HSL到RGB转换(适用于常见的HSL格式) + const hslMatch = hsl.match(/([\d.]+)\s+([\d.]+)%\s+([\d.]+)%/) + if (hslMatch) { + const h = parseFloat(hslMatch[1]) / 360 + const s = parseFloat(hslMatch[2]) / 100 + const l = parseFloat(hslMatch[3]) / 100 + + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1 + if (t > 1) t -= 1 + if (t < 1/6) return p + (q - p) * 6 * t + if (t < 1/2) return q + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6 + return p + } + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s + const p = 2 * l - q + const r = Math.round(hue2rgb(p, q, h + 1/3) * 255) + const g = Math.round(hue2rgb(p, q, h) * 255) + const b = Math.round(hue2rgb(p, q, h - 1/3) * 255) + + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` + } + + return '#000000' + } + + return { + graph: graphColor.startsWith('#') ? graphColor : convertHslToHex(foregroundColor) || '#000000', + foreground: convertHslToHex(foregroundColor) || '#000000', + background: convertHslToHex(backgroundColor) || '#ffffff' + } +} + +/** + * 获取SVG的计算样式 + * @param svgElement SVG元素 + * @returns CSS样式字符串 + */ +function getComputedStylesForSVG(svgElement: SVGElement): string { + const colors = getCurrentThemeColors() + const styles: string[] = [] + + // 添加基础样式,使用黑色作为导出颜色 + styles.push(` + .stroke-graph { stroke: #000000 !important; stroke-width: 1; } + .fill-transparent { fill: transparent !important; } + .text-foreground { fill: #000000 !important; } + .rounded-lg { rx: 8; ry: 8; } + .border { stroke: #000000 !important; stroke-width: 1; } + .font-mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; } + .text-center { text-anchor: middle; } + .whitespace-nowrap { white-space: nowrap; } + .leading-normal { line-height: 1.5; } + .pointer-events-none { pointer-events: none; } + text { fill: #000000 !important; } + path { stroke: #000000 !important; } + rect { stroke: #000000 !important; } + circle { stroke: #000000 !important; } + line { stroke: #000000 !important; } + `) + + return styles.join('\n') +} + +/** + * 下载Blob文件 + * @param blob Blob对象 + * @param filename 文件名 + */ +function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) +} + +/** + * 统一的导出函数 + * @param format 导出格式 + * @param element 要导出的元素 + * @param filename 文件名 + * @param options 导出选项 + */ +export const exportGraph = async ( + format: ExportFormat, + element: HTMLElement | SVGElement, + filename: string = 'regex-graph', + options: any = {} +) => { + switch (format) { + case 'svg': + if (element instanceof SVGElement) { + exportSVG(element, filename) + } else { + // 如果传入的不是SVG元素,尝试查找SVG子元素 + const svgElement = element.querySelector('svg') + if (svgElement) { + exportSVG(svgElement, filename) + } else { + throw new Error('未找到SVG元素') + } + } + break + default: + throw new Error(`不支持的导出格式: ${format}`) + } +} \ No newline at end of file diff --git a/src/modules/home/index.tsx b/src/modules/home/index.tsx index 0e5883d..b29c8a6 100644 --- a/src/modules/home/index.tsx +++ b/src/modules/home/index.tsx @@ -28,6 +28,8 @@ import { import { useToast } from '@/components/ui/use-toast' import { Toggle } from '@/components/ui/toggle' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { ExportDropdown, exportGraph } from '@/modules/export' +import type { ExportFormat } from '@/modules/export' function Home() { const [searchParams, setSearchParams] = useSearchParams() @@ -128,6 +130,37 @@ function Home() { toast({ description: t('Permalink copied.') }) } + const handleExport = async (format: ExportFormat) => { + try { + // 查找图形容器元素 + // 获取SVG的父容器,而不是SVG元素本身 + const svgElement = document.querySelector('[data-testid="graph"]') as SVGElement + const graphElement = svgElement?.parentElement || svgElement + if (!graphElement) { + toast({ + description: t('No graph to export'), + variant: 'destructive' + }) + return + } + + // 生成文件名 + const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-') + const filename = `regex-graph-${timestamp}` + + await exportGraph(format, graphElement, filename) + toast({ + description: t(`Graph exported as ${format.toUpperCase()}`) + }) + } catch (error) { + console.error('Export failed:', error) + toast({ + description: t('Export failed'), + variant: 'destructive' + }) + } + } + const graphShow = regex !== '' || (ast.body.length > 0 && !errorMsg) return (
- setEditorCollapsed(!pressed)} - > - - +
+ {graphShow && ( + + )} + setEditorCollapsed(!pressed)} + > + + +
{regex !== null && } From 2d6cf547a77eaee89f93e5ca50791475def6d992 Mon Sep 17 00:00:00 2001 From: persist-1 Date: Wed, 20 Aug 2025 18:42:35 +0800 Subject: [PATCH 02/14] Update: Improved "/samples" - New category added - Organize the samples according to their classification - Add more samples --- public/locales/cn/translation.json | 42 +++++ src/modules/samples/data.ts | 289 +++++++++++++++++++++++++++++ src/modules/samples/index.tsx | 184 ++++++++++++------ 3 files changed, 463 insertions(+), 52 deletions(-) create mode 100644 src/modules/samples/data.ts diff --git a/public/locales/cn/translation.json b/public/locales/cn/translation.json index 3a3f69f..273483b 100644 --- a/public/locales/cn/translation.json +++ b/public/locales/cn/translation.json @@ -111,6 +111,48 @@ "3. Whole + Decimal Numbers": "3. 整数 + 小数", "4. Negative, Positive Whole + Decimal Numbers": "4. 正负 整数 + 小数", "6. Date Format YYYY-MM-dd": "6. 日期格式 YYYY-MM-dd", + "Numbers": "数字", + "URLs": "网址", + "Dates": "日期", + "Phone Numbers": "电话号码", + "IP Addresses": "IP地址", + "HTML Tags": "HTML标签", + "Email Addresses": "邮箱地址", + "Passwords": "密码", + "ID Numbers": "证件号码", + "All Categories": "所有分类", + "Whole Numbers": "整数", + "Decimal Numbers": "小数", + "Whole + Decimal Numbers": "整数和小数", + "Negative, Positive Whole + Decimal Numbers": "正负整数和小数", + "Currency Amount": "货币金额", + "Percentage": "百分比", + "Basic URL": "基本URL", + "Domain Name": "域名", + "FTP URL": "FTP地址", + "URL with Port": "带端口的URL", + "Date Format YYYY-MM-DD": "日期格式 YYYY-MM-DD", + "Date Format DD/MM/YYYY": "日期格式 DD/MM/YYYY", + "Date Format MM/DD/YYYY": "日期格式 MM/DD/YYYY", + "Time Format HH:MM": "时间格式 HH:MM", + "DateTime ISO 8601": "ISO 8601日期时间", + "Chinese Mobile Phone": "中国手机号", + "US Phone Number": "美国电话号码", + "International Phone": "国际电话号码", + "Chinese Landline": "中国固定电话", + "IPv4 Address": "IPv4地址", + "IPv6 Address": "IPv6地址", + "MAC Address": "MAC地址", + "HTML Tag": "HTML标签", + "HTML Tag with Attributes": "带属性的HTML标签", + "HTML Comment": "HTML注释", + "HTML Image Tag": "HTML图片标签", + "Basic Email": "基本邮箱", + "Strict Email RFC 5322": "严格邮箱格式 RFC 5322", + "Strong Password": "强密码", + "Medium Password": "中等强度密码", + "Chinese ID Card": "中国身份证", + "Credit Card Number": "信用卡号码", "Flags: ": "标志: ", "Allows . to match newline": "允许 . 匹配换行符", "Settings: ": "设置: ", diff --git a/src/modules/samples/data.ts b/src/modules/samples/data.ts new file mode 100644 index 0000000..89b3f34 --- /dev/null +++ b/src/modules/samples/data.ts @@ -0,0 +1,289 @@ +// 正则表达式样例数据 +export interface RegexSample { + desc: string + label: string + regex: string + explanation?: string +} + +export interface RegexCategory { + id: string + name: string + icon: string + samples: RegexSample[] +} + +// 正则表达式分类数据 +export const regexCategories: RegexCategory[] = [ + { + id: 'numbers', + name: 'Numbers', + icon: '🔢', + samples: [ + { + desc: 'Whole Numbers', + label: '/^\\d+$/', + regex: '^\\d+$', + explanation: '匹配整数' + }, + { + desc: 'Decimal Numbers', + label: '/^\\d*\\.\\d+$/', + regex: '^\\d*\\.\\d+$', + explanation: '匹配小数' + }, + { + desc: 'Whole + Decimal Numbers', + label: '/^\\d*(\\.\\d+)?$/', + regex: '^\\d*(\\.\\d+)?$', + explanation: '匹配整数和小数' + }, + { + desc: 'Negative, Positive Whole + Decimal Numbers', + label: '/^-?\\d*(\\.\\d+)?$/', + regex: '^-?\\d*(\\.\\d+)?$', + explanation: '匹配正负整数和小数' + }, + { + desc: 'Currency Amount', + label: '/^\\$?\\d{1,3}(,\\d{3})*(\\.\\d{2})?$/', + regex: '^\\$?\\d{1,3}(,\\d{3})*(\\.\\d{2})?$', + explanation: '匹配货币金额格式' + }, + { + desc: 'Percentage', + label: '/^\\d{1,3}(\\.\\d{1,2})?%$/', + regex: '^\\d{1,3}(\\.\\d{1,2})?%$', + explanation: '匹配百分比' + } + ] + }, + { + id: 'urls', + name: 'URLs', + icon: '🌐', + samples: [ + { + desc: 'Basic URL', + label: '/^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$/', + regex: '^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$', + explanation: '匹配基本的HTTP/HTTPS URL' + }, + { + desc: 'Domain Name', + label: '/^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$/', + regex: '^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$', + explanation: '匹配域名' + }, + { + desc: 'FTP URL', + label: '/^ftp:\\/\\/[\\w\\.-]+\\.[a-zA-Z]{2,}(:\\d+)?(\\/.*)?$/', + regex: '^ftp:\\/\\/[\\w\\.-]+\\.[a-zA-Z]{2,}(:\\d+)?(\\/.*)?$', + explanation: '匹配FTP URL' + }, + { + desc: 'URL with Port', + label: '/^https?:\\/\\/[\\w\\.-]+(:\\d+)?(\\/.*)?$/', + regex: '^https?:\\/\\/[\\w\\.-]+(:\\d+)?(\\/.*)?$', + explanation: '匹配带端口号的URL' + } + ] + }, + { + id: 'dates', + name: 'Dates', + icon: '📅', + samples: [ + { + desc: 'Date Format YYYY-MM-DD', + label: '/^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/', + regex: '^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$', + explanation: '匹配YYYY-MM-DD格式日期' + }, + { + desc: 'Date Format DD/MM/YYYY', + label: '/^(0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/[12]\\d{3}$/', + regex: '^(0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/[12]\\d{3}$', + explanation: '匹配DD/MM/YYYY格式日期' + }, + { + desc: 'Date Format MM/DD/YYYY', + label: '/^(0[1-9]|1[0-2])\\/(0[1-9]|[12]\\d|3[01])\\/[12]\\d{3}$/', + regex: '^(0[1-9]|1[0-2])\\/(0[1-9]|[12]\\d|3[01])\\/[12]\\d{3}$', + explanation: '匹配MM/DD/YYYY格式日期' + }, + { + desc: 'Time Format HH:MM', + label: '/^([01]?\\d|2[0-3]):[0-5]\\d$/', + regex: '^([01]?\\d|2[0-3]):[0-5]\\d$', + explanation: '匹配24小时制时间格式' + }, + { + desc: 'DateTime ISO 8601', + label: '/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$/', + regex: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$', + explanation: '匹配ISO 8601日期时间格式' + } + ] + }, + { + id: 'phones', + name: 'Phone Numbers', + icon: '📞', + samples: [ + { + desc: 'Chinese Mobile Phone', + label: '/^1[3-9]\\d{9}$/', + regex: '^1[3-9]\\d{9}$', + explanation: '匹配中国大陆手机号码' + }, + { + desc: 'US Phone Number', + label: '/^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/', + regex: '^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$', + explanation: '匹配美国电话号码格式' + }, + { + desc: 'International Phone', + label: '/^\\+?[1-9]\\d{1,14}$/', + regex: '^\\+?[1-9]\\d{1,14}$', + explanation: '匹配国际电话号码格式' + }, + { + desc: 'Chinese Landline', + label: '/^0\\d{2,3}-?\\d{7,8}$/', + regex: '^0\\d{2,3}-?\\d{7,8}$', + explanation: '匹配中国固定电话号码' + } + ] + }, + { + id: 'ips', + name: 'IP Addresses', + icon: '🌍', + samples: [ + { + desc: 'IPv4 Address', + label: '/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/', + regex: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', + explanation: '匹配IPv4地址' + }, + { + desc: 'IPv6 Address', + label: '/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/', + regex: '^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$', + explanation: '匹配完整IPv6地址' + }, + { + desc: 'MAC Address', + label: '/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/', + regex: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', + explanation: '匹配MAC地址' + } + ] + }, + { + id: 'html', + name: 'HTML Tags', + icon: '🏷️', + samples: [ + { + desc: 'HTML Tag', + label: '/<\\/?[a-zA-Z][a-zA-Z0-9]*[^<>]*>/', + regex: '<\\/?[a-zA-Z][a-zA-Z0-9]*[^<>]*>', + explanation: '匹配HTML标签' + }, + { + desc: 'HTML Tag with Attributes', + label: '/<([a-zA-Z][a-zA-Z0-9]*)\\b[^>]*>(.*?)<\\/\\1>/', + regex: '<([a-zA-Z][a-zA-Z0-9]*)\\b[^>]*>(.*?)<\\/\\1>', + explanation: '匹配带属性的HTML标签对' + }, + { + desc: 'HTML Comment', + label: '//', + regex: '', + explanation: '匹配HTML注释' + }, + { + desc: 'HTML Image Tag', + label: '/]*src\\s*=\\s*["\']([^"\'>]+)["\'][^>]*>/', + regex: ']*src\\s*=\\s*["\']([^"\'>]+)["\'][^>]*>', + explanation: '匹配HTML图片标签并提取src属性' + } + ] + }, + { + id: 'emails', + name: 'Email Addresses', + icon: '📧', + samples: [ + { + desc: 'Basic Email', + label: '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/', + regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + explanation: '匹配基本邮箱格式' + }, + { + desc: 'Strict Email RFC 5322', + label: '/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/', + regex: '^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$', + explanation: '严格的RFC 5322邮箱格式' + } + ] + }, + { + id: 'passwords', + name: 'Passwords', + icon: '🔒', + samples: [ + { + desc: 'Strong Password', + label: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$/', + regex: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$', + explanation: '强密码:至少8位,包含大小写字母、数字和特殊字符' + }, + { + desc: 'Medium Password', + label: '/^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{6,}$/', + regex: '^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{6,}$', + explanation: '中等强度密码:至少6位,包含字母和数字' + } + ] + }, + { + id: 'identifiers', + name: 'ID Numbers', + icon: '🆔', + samples: [ + { + desc: 'Chinese ID Card', + label: '/^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$/', + regex: '^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$', + explanation: '匹配中国身份证号码' + }, + { + desc: 'Credit Card Number', + label: '/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$/', + regex: '^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$', + explanation: '匹配信用卡号码(Visa、MasterCard、AmEx等)' + } + ] + } +] + +// 获取所有样例的扁平化列表 +export function getAllSamples(): RegexSample[] { + return regexCategories.flatMap(category => category.samples) +} + +// 根据分类ID获取样例 +export function getSamplesByCategory(categoryId: string): RegexSample[] { + const category = regexCategories.find(cat => cat.id === categoryId) + return category ? category.samples : [] +} + +// 获取分类信息 +export function getCategoryById(categoryId: string): RegexCategory | undefined { + return regexCategories.find(cat => cat.id === categoryId) +} \ No newline at end of file diff --git a/src/modules/samples/index.tsx b/src/modules/samples/index.tsx index 6971d8b..3219bd7 100644 --- a/src/modules/samples/index.tsx +++ b/src/modules/samples/index.tsx @@ -1,65 +1,145 @@ +import { useState } from 'react' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' +import clsx from 'clsx' import SimpleGraph from '@/modules/graph/simple-graph' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { Button } from '@/components/ui/button' +import { regexCategories, getAllSamples, getSamplesByCategory } from './data' +import type { RegexSample } from './data' -const samples = [ - { desc: '1. Whole Numbers', label: '/^\\d+$/', regex: '^\\d+$' }, - { - desc: '2. Decimal Numbers', - label: '/^\\d*\\.\\d+$/', - regex: '^\\d*\\.\\d+$', - }, - { - desc: '3. Whole + Decimal Numbers', - label: '/^\\d*(\\.\\d+)?$/', - regex: '^\\d*(\\.\\d+)?$', - }, - { - desc: '4. Negative, Positive Whole + Decimal Numbers', - label: '/^-?\\d*(\\.\\d+)?$/', - regex: '^-?\\d*(\\.\\d+)?$', - }, - { - desc: '5. Url', - label: - '/^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$/', - regex: - '^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$', - }, - { - desc: '6. Date Format YYYY-MM-dd', - label: '/^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/', - regex: '^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$', - }, -] function Samples() { const { t } = useTranslation() + const [selectedCategory, setSelectedCategory] = useState('all') + + // 获取当前选中分类的样例 + const currentSamples: RegexSample[] = selectedCategory === 'all' + ? getAllSamples() + : getSamplesByCategory(selectedCategory) + return ( - -
-
- {samples.map(({ desc, label, regex }) => { - const linkTo = `/?r=${encodeURIComponent(`/${regex}/`)}` - return ( -
- - {t(desc)} - : - {label} - - - - - - - -
- ) - })} +
+ {/* 左侧分类栏 */} +
+
+

+ {t('Samples')} +

+
+ {/* 全部分类按钮 */} + + + {/* 分类按钮 */} + {regexCategories.map((category) => ( + + ))} +
- + + {/* 右侧内容区域 */} +
+ +
+
+ {/* 分类标题 */} +
+

+ {selectedCategory === 'all' + ? t('All Categories') + : t(regexCategories.find(cat => cat.id === selectedCategory)?.name || '') + } +

+

+ {selectedCategory === 'all' + ? `共 ${currentSamples.length} 个正则表达式样例` + : `${currentSamples.length} 个样例` + } +

+
+ + {/* 样例列表 */} +
+ {currentSamples.map((sample, index) => { + const linkTo = `/?r=${encodeURIComponent(`/${sample.regex}/`)}` + return ( +
+ {/* 样例标题和描述 */} +
+ +

+ {t(sample.desc)} +

+

+ {sample.explanation} +

+ +
+ + {/* 正则表达式 */} +
+ + + {sample.label} + + +
+ + {/* 可视化图形 */} +
+ + +
+ +
+ + +
+
+
+ ) + })} +
+ + {/* 空状态 */} + {currentSamples.length === 0 && ( +
+
📝
+

+ 该分类暂无样例 +

+
+ )} +
+
+
+
+
) } From 5700d121100d78166559652b3bb7e9683cef6585 Mon Sep 17 00:00:00 2001 From: persist-1 Date: Wed, 20 Aug 2025 18:47:37 +0800 Subject: [PATCH 03/14] update: updated translation about "/samples" text --- public/locales/jp/translation.json | 42 ++++++++++++++++++++++++++++++ public/locales/ru/translation.json | 42 ++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/public/locales/jp/translation.json b/public/locales/jp/translation.json index 1a6ab12..185ac2c 100644 --- a/public/locales/jp/translation.json +++ b/public/locales/jp/translation.json @@ -114,6 +114,48 @@ "3. Whole + Decimal Numbers": "3. 整数と小数", "4. Negative, Positive Whole + Decimal Numbers": "4. 正負の整数と小数", "6. Date Format YYYY-MM-dd": "6. 日付形式 YYYY-MM-dd", + "Numbers": "数字", + "URLs": "URL", + "Dates": "日付", + "Phone Numbers": "電話番号", + "IP Addresses": "IPアドレス", + "HTML Tags": "HTMLタグ", + "Email Addresses": "メールアドレス", + "Passwords": "パスワード", + "ID Numbers": "ID番号", + "All Categories": "すべてのカテゴリ", + "Whole Numbers": "整数", + "Decimal Numbers": "小数", + "Whole + Decimal Numbers": "整数と小数", + "Negative, Positive Whole + Decimal Numbers": "正負の整数と小数", + "Currency Amount": "通貨金額", + "Percentage": "パーセンテージ", + "Basic URL": "基本URL", + "Domain Name": "ドメイン名", + "FTP URL": "FTP URL", + "URL with Port": "ポート付きURL", + "Date Format YYYY-MM-DD": "日付形式 YYYY-MM-DD", + "Date Format DD/MM/YYYY": "日付形式 DD/MM/YYYY", + "Date Format MM/DD/YYYY": "日付形式 MM/DD/YYYY", + "Time Format HH:MM": "時刻形式 HH:MM", + "DateTime ISO 8601": "ISO 8601日時", + "Chinese Mobile Phone": "中国携帯電話", + "US Phone Number": "米国電話番号", + "International Phone": "国際電話", + "Chinese Landline": "中国固定電話", + "IPv4 Address": "IPv4アドレス", + "IPv6 Address": "IPv6アドレス", + "MAC Address": "MACアドレス", + "HTML Tag": "HTMLタグ", + "HTML Tag with Attributes": "属性付きHTMLタグ", + "HTML Comment": "HTMLコメント", + "HTML Image Tag": "HTML画像タグ", + "Basic Email": "基本メール", + "Strict Email RFC 5322": "厳密メール RFC 5322", + "Strong Password": "強力パスワード", + "Medium Password": "中程度パスワード", + "Chinese ID Card": "中国身分証", + "Credit Card Number": "クレジットカード番号", "Flags: ": "フラグ: ", "Flag: ": "フラグ: ", "Allows . to match newline": "ドットが改行に一致することを許可", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 0d41d6e..14aa280 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -114,6 +114,48 @@ "3. Whole + Decimal Numbers": "3. Неотрицательные целые и действительные числа", "4. Negative, Positive Whole + Decimal Numbers": "4. Действительные числа", "6. Date Format YYYY-MM-dd": "6. Дата в формате YYYY-MM-dd", + "Numbers": "Числа", + "URLs": "URL-адреса", + "Dates": "Даты", + "Phone Numbers": "Номера телефонов", + "IP Addresses": "IP-адреса", + "HTML Tags": "HTML-теги", + "Email Addresses": "Адреса электронной почты", + "Passwords": "Пароли", + "ID Numbers": "Идентификационные номера", + "All Categories": "Все категории", + "Whole Numbers": "Целые числа", + "Decimal Numbers": "Десятичные числа", + "Whole + Decimal Numbers": "Целые и десятичные числа", + "Negative, Positive Whole + Decimal Numbers": "Положительные и отрицательные числа", + "Currency Amount": "Денежная сумма", + "Percentage": "Проценты", + "Basic URL": "Базовый URL", + "Domain Name": "Доменное имя", + "FTP URL": "FTP URL", + "URL with Port": "URL с портом", + "Date Format YYYY-MM-DD": "Формат даты YYYY-MM-DD", + "Date Format DD/MM/YYYY": "Формат даты DD/MM/YYYY", + "Date Format MM/DD/YYYY": "Формат даты MM/DD/YYYY", + "Time Format HH:MM": "Формат времени HH:MM", + "DateTime ISO 8601": "Дата и время ISO 8601", + "Chinese Mobile Phone": "Китайский мобильный телефон", + "US Phone Number": "Американский номер телефона", + "International Phone": "Международный телефон", + "Chinese Landline": "Китайский стационарный телефон", + "IPv4 Address": "IPv4 адрес", + "IPv6 Address": "IPv6 адрес", + "MAC Address": "MAC адрес", + "HTML Tag": "HTML тег", + "HTML Tag with Attributes": "HTML тег с атрибутами", + "HTML Comment": "HTML комментарий", + "HTML Image Tag": "HTML тег изображения", + "Basic Email": "Базовый email", + "Strict Email RFC 5322": "Строгий email RFC 5322", + "Strong Password": "Сильный пароль", + "Medium Password": "Средний пароль", + "Chinese ID Card": "Китайское удостоверение личности", + "Credit Card Number": "Номер кредитной карты", "Flag: ": "Флаги: ", "Allows . to match newline": "Считать . символом перевода строки", "Settings: ": "Настройки: ", From 92ecaf8f6b85aab74001d9fd9f8d9167d90d2fa3 Mon Sep 17 00:00:00 2001 From: persist-1 Date: Thu, 21 Aug 2025 19:21:15 +0800 Subject: [PATCH 04/14] fix: - all 'console.log' has been removed to make the code clearer and more concise - removed the redundant comments - Convert all Chineset comments to English and make the English comments the default comments - Whether it is comments or front-end text, English is set as the default - Optimized the decoding of special symbols in the export as svg function - The style effect and layout design of the samples page have been optimized --- package.json | 2 +- pnpm-lock.yaml | 181 ++++++----------- src/modules/export/index.ts | 2 +- src/modules/export/utils.ts | 355 +++++++++++----------------------- src/modules/home/index.tsx | 7 +- src/modules/samples/data.ts | 74 +++---- src/modules/samples/index.tsx | 90 ++++----- 7 files changed, 253 insertions(+), 458 deletions(-) diff --git a/package.json b/package.json index 7d9dc1f..129e432 100644 --- a/package.json +++ b/package.json @@ -63,10 +63,10 @@ }, "devDependencies": { "@antfu/eslint-config": "^2.22.2", + "@swc/core": "^1.13.4", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", - "@types/html2canvas": "^1.0.0", "@types/node": "^18.0.0", "@types/react": "^18.0.11", "@types/react-dom": "^18.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82a1bf6..d40f011 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,10 +97,10 @@ importers: version: 2.4.0 tailwindcss: specifier: ^3.4.4 - version: 3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)) + version: 3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)) tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3))) + version: 1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3))) typescript: specifier: ^5.5.0 version: 5.5.3 @@ -123,6 +123,9 @@ importers: '@antfu/eslint-config': specifier: ^2.22.2 version: 2.22.2(@eslint-react/eslint-plugin@1.5.25(eslint@9.7.0)(typescript@5.5.3))(@vue/compiler-sfc@3.4.31)(eslint-plugin-react-hooks@5.1.0-rc-df5f2736-20240712(eslint@9.7.0))(eslint-plugin-react-refresh@0.4.8(eslint@9.7.0))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.5) + '@swc/core': + specifier: ^1.13.4 + version: 1.13.4 '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -132,9 +135,6 @@ importers: '@testing-library/react': specifier: ^16.0.0 version: 16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/html2canvas': - specifier: ^1.0.0 - version: 1.0.0 '@types/node': specifier: ^18.0.0 version: 18.19.39 @@ -149,7 +149,7 @@ importers: version: 2.2.9 '@vitejs/plugin-react-swc': specifier: ^3.7.0 - version: 3.7.0(@swc/helpers@0.5.12)(vite@5.3.3(@types/node@18.19.39)) + version: 3.7.0(vite@5.3.3(@types/node@18.19.39)) '@vitest/ui': specifier: ^2.0.5 version: 2.0.5(vitest@2.0.5) @@ -1118,71 +1118,71 @@ packages: peerDependencies: eslint: '>=8.40.0' - '@swc/core-darwin-arm64@1.6.13': - resolution: {integrity: sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==} + '@swc/core-darwin-arm64@1.13.4': + resolution: {integrity: sha512-CGbTu9dGBwgklUj+NAQAYyPjBuoHaNRWK4QXJRv1QNIkhtE27aY7QA9uEON14SODxsio3t8+Pjjl2Mzx1Pxf+g==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.6.13': - resolution: {integrity: sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==} + '@swc/core-darwin-x64@1.13.4': + resolution: {integrity: sha512-qLFwYmLrqHNCf+JO9YLJT6IP/f9LfbXILTaqyfluFLW1GCfJyvUrSt3CWaL2lwwyT1EbBh6BVaAAecXiJIo3vg==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.6.13': - resolution: {integrity: sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==} + '@swc/core-linux-arm-gnueabihf@1.13.4': + resolution: {integrity: sha512-y7SeNIA9em3+smNMpr781idKuNwJNAqewiotv+pIR5FpXdXXNjHWW+jORbqQYd61k6YirA5WQv+Af4UzqEX17g==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.6.13': - resolution: {integrity: sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==} + '@swc/core-linux-arm64-gnu@1.13.4': + resolution: {integrity: sha512-u0c51VdzRmXaphLgghY9+B2Frzler6nIv+J788nqIh6I0ah3MmMW8LTJKZfdaJa3oFxzGNKXsJiaU2OFexNkug==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.6.13': - resolution: {integrity: sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==} + '@swc/core-linux-arm64-musl@1.13.4': + resolution: {integrity: sha512-Z92GJ98x8yQHn4I/NPqwAQyHNkkMslrccNVgFcnY1msrb6iGSw5uFg2H2YpvQ5u2/Yt6CRpLIUVVh8SGg1+gFA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.6.13': - resolution: {integrity: sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==} + '@swc/core-linux-x64-gnu@1.13.4': + resolution: {integrity: sha512-rSUcxgpFF0L8Fk1CbUf946XCX1CRp6eaHfKqplqFNWCHv8HyqAtSFvgCHhT+bXru6Ca/p3sLC775SUeSWhsJ9w==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.6.13': - resolution: {integrity: sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==} + '@swc/core-linux-x64-musl@1.13.4': + resolution: {integrity: sha512-qY77eFUvmdXNSmTW+I1fsz4enDuB0I2fE7gy6l9O4koSfjcCxkXw2X8x0lmKLm3FRiINS1XvZSg2G+q4NNQCRQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.6.13': - resolution: {integrity: sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==} + '@swc/core-win32-arm64-msvc@1.13.4': + resolution: {integrity: sha512-xjPeDrOf6elCokxuyxwoskM00JJFQMTT2hTQZE24okjG3JiXzSFV+TmzYSp+LWNxPpnufnUUy/9Ee8+AcpslGw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.6.13': - resolution: {integrity: sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==} + '@swc/core-win32-ia32-msvc@1.13.4': + resolution: {integrity: sha512-Ta+Bblc9tE9X9vQlpa3r3+mVnHYdKn09QsZ6qQHvuXGKWSS99DiyxKTYX2vxwMuoTObR0BHvnhNbaGZSV1VwNA==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.6.13': - resolution: {integrity: sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==} + '@swc/core-win32-x64-msvc@1.13.4': + resolution: {integrity: sha512-pHnb4QwGiuWs4Z9ePSgJ48HP3NZIno6l75SB8YLCiPVDiLhvCLKEjz/caPRsFsmet9BEP8e3bAf2MV8MXgaTSg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.6.13': - resolution: {integrity: sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==} + '@swc/core@1.13.4': + resolution: {integrity: sha512-bCq2GCuKV16DSOOEdaRqHMm1Ok4YEoLoNdgdzp8BS/Hxxr/0NVCHBUgRLLRy/TlJGv20Idx+djd5FIDvsnqMaw==} engines: {node: '>=10'} peerDependencies: - '@swc/helpers': '*' + '@swc/helpers': '>=0.5.17' peerDependenciesMeta: '@swc/helpers': optional: true @@ -1190,11 +1190,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.12': - resolution: {integrity: sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==} - - '@swc/types@0.1.9': - resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==} + '@swc/types@0.1.24': + resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -1240,10 +1237,6 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/html2canvas@1.0.0': - resolution: {integrity: sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==} - deprecated: This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed. - '@types/js-cookie@2.2.7': resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} @@ -1548,10 +1541,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-arraybuffer@1.0.2: - resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} - engines: {node: '>= 0.6.0'} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1702,9 +1691,6 @@ packages: css-in-js-utils@3.1.0: resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} - css-line-break@2.1.0: - resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} - css-tree@1.1.3: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} @@ -2275,10 +2261,6 @@ packages: html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} - html2canvas@1.4.1: - resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} - engines: {node: '>=8.0.0'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3210,9 +3192,6 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} - text-segmentation@1.0.3: - resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} - text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -3399,9 +3378,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utrie@1.0.2: - resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -4576,61 +4552,55 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.6.13': + '@swc/core-darwin-arm64@1.13.4': optional: true - '@swc/core-darwin-x64@1.6.13': + '@swc/core-darwin-x64@1.13.4': optional: true - '@swc/core-linux-arm-gnueabihf@1.6.13': + '@swc/core-linux-arm-gnueabihf@1.13.4': optional: true - '@swc/core-linux-arm64-gnu@1.6.13': + '@swc/core-linux-arm64-gnu@1.13.4': optional: true - '@swc/core-linux-arm64-musl@1.6.13': + '@swc/core-linux-arm64-musl@1.13.4': optional: true - '@swc/core-linux-x64-gnu@1.6.13': + '@swc/core-linux-x64-gnu@1.13.4': optional: true - '@swc/core-linux-x64-musl@1.6.13': + '@swc/core-linux-x64-musl@1.13.4': optional: true - '@swc/core-win32-arm64-msvc@1.6.13': + '@swc/core-win32-arm64-msvc@1.13.4': optional: true - '@swc/core-win32-ia32-msvc@1.6.13': + '@swc/core-win32-ia32-msvc@1.13.4': optional: true - '@swc/core-win32-x64-msvc@1.6.13': + '@swc/core-win32-x64-msvc@1.13.4': optional: true - '@swc/core@1.6.13(@swc/helpers@0.5.12)': + '@swc/core@1.13.4': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.9 + '@swc/types': 0.1.24 optionalDependencies: - '@swc/core-darwin-arm64': 1.6.13 - '@swc/core-darwin-x64': 1.6.13 - '@swc/core-linux-arm-gnueabihf': 1.6.13 - '@swc/core-linux-arm64-gnu': 1.6.13 - '@swc/core-linux-arm64-musl': 1.6.13 - '@swc/core-linux-x64-gnu': 1.6.13 - '@swc/core-linux-x64-musl': 1.6.13 - '@swc/core-win32-arm64-msvc': 1.6.13 - '@swc/core-win32-ia32-msvc': 1.6.13 - '@swc/core-win32-x64-msvc': 1.6.13 - '@swc/helpers': 0.5.12 + '@swc/core-darwin-arm64': 1.13.4 + '@swc/core-darwin-x64': 1.13.4 + '@swc/core-linux-arm-gnueabihf': 1.13.4 + '@swc/core-linux-arm64-gnu': 1.13.4 + '@swc/core-linux-arm64-musl': 1.13.4 + '@swc/core-linux-x64-gnu': 1.13.4 + '@swc/core-linux-x64-musl': 1.13.4 + '@swc/core-win32-arm64-msvc': 1.13.4 + '@swc/core-win32-ia32-msvc': 1.13.4 + '@swc/core-win32-x64-msvc': 1.13.4 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.12': - dependencies: - tslib: 2.6.3 - optional: true - - '@swc/types@0.1.9': + '@swc/types@0.1.24': dependencies: '@swc/counter': 0.1.3 @@ -4687,10 +4657,6 @@ snapshots: '@types/estree@1.0.5': {} - '@types/html2canvas@1.0.0': - dependencies: - html2canvas: 1.4.1 - '@types/js-cookie@2.2.7': {} '@types/json-schema@7.0.15': {} @@ -4898,9 +4864,9 @@ snapshots: optionalDependencies: react: 18.3.1 - '@vitejs/plugin-react-swc@3.7.0(@swc/helpers@0.5.12)(vite@5.3.3(@types/node@18.19.39))': + '@vitejs/plugin-react-swc@3.7.0(vite@5.3.3(@types/node@18.19.39))': dependencies: - '@swc/core': 1.6.13(@swc/helpers@0.5.12) + '@swc/core': 1.13.4 vite: 5.3.3(@types/node@18.19.39) transitivePeerDependencies: - '@swc/helpers' @@ -5067,8 +5033,6 @@ snapshots: balanced-match@1.0.2: {} - base64-arraybuffer@1.0.2: {} - binary-extensions@2.3.0: {} boolbase@1.0.0: {} @@ -5220,10 +5184,6 @@ snapshots: dependencies: hyphenate-style-name: 1.1.0 - css-line-break@2.1.0: - dependencies: - utrie: 1.0.2 - css-tree@1.1.3: dependencies: mdn-data: 2.0.14 @@ -5927,11 +5887,6 @@ snapshots: dependencies: void-elements: 3.1.0 - html2canvas@1.4.1: - dependencies: - css-line-break: 2.1.0 - text-segmentation: 1.0.3 - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 @@ -6437,13 +6392,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.39 - postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)): + postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)): dependencies: lilconfig: 3.1.2 yaml: 2.4.5 optionalDependencies: postcss: 8.4.39 - ts-node: 10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3) + ts-node: 10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3) postcss-nested@6.0.1(postcss@8.4.39): dependencies: @@ -6825,11 +6780,11 @@ snapshots: tailwind-merge@2.4.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3))): + tailwindcss-animate@1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3))): dependencies: - tailwindcss: 3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)) + tailwindcss: 3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)) - tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)): + tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -6848,7 +6803,7 @@ snapshots: postcss: 8.4.39 postcss-import: 15.1.0(postcss@8.4.39) postcss-js: 4.0.1(postcss@8.4.39) - postcss-load-config: 4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3)) + postcss-load-config: 4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3)) postcss-nested: 6.0.1(postcss@8.4.39) postcss-selector-parser: 6.1.1 resolve: 1.22.8 @@ -6858,10 +6813,6 @@ snapshots: tapable@2.2.1: {} - text-segmentation@1.0.3: - dependencies: - utrie: 1.0.2 - text-table@0.2.0: {} thenify-all@1.6.0: @@ -6923,7 +6874,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.12))(@types/node@18.19.39)(typescript@5.5.3): + ts-node@10.9.2(@swc/core@1.13.4)(@types/node@18.19.39)(typescript@5.5.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -6941,7 +6892,7 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.6.13(@swc/helpers@0.5.12) + '@swc/core': 1.13.4 optional: true tsconfck@3.1.1(typescript@5.5.3): @@ -7013,10 +6964,6 @@ snapshots: util-deprecate@1.0.2: {} - utrie@1.0.2: - dependencies: - base64-arraybuffer: 1.0.2 - v8-compile-cache-lib@3.0.1: optional: true diff --git a/src/modules/export/index.ts b/src/modules/export/index.ts index 5aacf82..fe513c6 100644 --- a/src/modules/export/index.ts +++ b/src/modules/export/index.ts @@ -1,4 +1,4 @@ -// 导出功能模块统一入口 +// Export functionality module unified entry point export { default as ExportDropdown } from './dropdown' export type { ExportFormat } from './dropdown' export { exportGraph, exportSVG } from './utils' diff --git a/src/modules/export/utils.ts b/src/modules/export/utils.ts index d0d219a..bf28663 100644 --- a/src/modules/export/utils.ts +++ b/src/modules/export/utils.ts @@ -1,159 +1,75 @@ export type ExportFormat = 'svg' +// Export configuration constants +const EXPORT_CONFIG = { + DEFAULT_FILENAME: 'regex-graph', + SVG_NAMESPACE: 'http://www.w3.org/2000/svg', + XLINK_NAMESPACE: 'http://www.w3.org/1999/xlink', + DEFAULT_FONT_SIZE: '15', + DEFAULT_ICON_WIDTH: 12, + DEFAULT_ICON_HEIGHT: 18, + + DEFAULT_COLORS: { + BLACK: '#000000', + WHITE: '#ffffff', + DARK_TEXT: '#111827' + }, + SVG_PATHS: { + // Phosphor Icons Infinity path + INFINITY: 'M248,128a56,56,0,0,1-96,39.6L83.33,96.17A40,40,0,1,0,83.33,159.83L152,231.6A56,56,0,1,1,152,24.4L83.33,96.17a40,40,0,1,1,0,63.66L152,231.6A56.09,56.09,0,0,1,248,128Z', + // Quantifier repetition arrow paths + QUANTIFIER_PATHS: [ + 'M17 1l4 4-4 4', + 'M3 11V9a4 4 0 014-4h14M21 13v2a4 4 0 01-4 4H3', + 'M7 23l-4-4 4-4' + ] + } +} as const + /** - * 导出SVG格式 - * @param svgElement SVG元素 - * @param filename 文件名 + * Export SVG format + * @param svgElement SVG element + * @param filename File name */ -export const exportSVG = (svgElement: SVGElement, filename: string = 'regex-graph') => { +export const exportSVG = (svgElement: SVGElement, filename: string = EXPORT_CONFIG.DEFAULT_FILENAME) => { try { - console.log('=== 开始SVG导出处理 ===') - console.log('原始SVG元素:', svgElement) - - // 克隆SVG元素以避免修改原始元素 + // Clone SVG element to avoid modifying the original const clonedSvg = svgElement.cloneNode(true) as SVGElement - // 设置SVG的xmlns属性以确保独立性 - clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg') - clonedSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink') - - // 处理foreignObject元素,将其转换为原生SVG文本元素 - const foreignObjects = clonedSvg.querySelectorAll('foreignObject') - console.log(`找到 ${foreignObjects.length} 个 foreignObject 元素`) - foreignObjects.forEach((fo, index) => { - console.log(`处理 foreignObject ${index + 1}:`, fo) - const div = fo.querySelector('div') - if (div) { - // 智能提取文本内容,包括处理图标元素 - console.log('div内容:', div.innerHTML) - const extractedContent = extractTextWithIcons(div) - console.log('提取的内容:', extractedContent) - const x = parseFloat(fo.getAttribute('x') || '0') - const y = parseFloat(fo.getAttribute('y') || '0') - const width = parseFloat(fo.getAttribute('width') || '0') - const height = parseFloat(fo.getAttribute('height') || '0') - - // 创建SVG text元素替换foreignObject - const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text') - textElement.setAttribute('x', (x + width / 2).toString()) - textElement.setAttribute('y', (y + height / 2 + 6).toString()) // 调整垂直居中 - textElement.setAttribute('text-anchor', 'middle') - textElement.setAttribute('dominant-baseline', 'middle') - textElement.setAttribute('font-family', 'ui-monospace, monospace') - textElement.setAttribute('font-size', fo.getAttribute('font-size') || '16') - textElement.setAttribute('fill', '#111827') - // 确保特殊Unicode字符(如∞)能正确显示 - textElement.setAttribute('unicode-bidi', 'embed') - textElement.setAttribute('direction', 'ltr') - textElement.textContent = extractedContent - - // 替换foreignObject - fo.parentNode?.replaceChild(textElement, fo) - } - }) - - // 处理主SVG中的独立图标元素(如循环图标) - const allSvgs = clonedSvg.querySelectorAll('svg') - console.log(`导出处理:找到 ${allSvgs.length} 个SVG元素`) - - allSvgs.forEach((iconSvg, index) => { - // 跳过主SVG容器本身 - if (iconSvg === clonedSvg) { - console.log(`跳过主SVG容器 (索引 ${index})`) - return - } - - const paths = iconSvg.querySelectorAll('path') - let isLoopIcon = false - - console.log(`检查SVG ${index},包含 ${paths.length} 个path元素`) - - // 检测循环图标 - for (let i = 0; i < paths.length; i++) { - const path = paths[i] - const d = path.getAttribute('d') || '' - console.log(`Path ${i}: ${d.substring(0, 50)}...`) - - if ((d.includes('M17 1l4 4-4 4') || d.includes('M7 23l-4-4 4-4')) || - (d.includes('M3 11V9a4 4') && d.includes('M21 13v2a4 4')) || - (d.includes('l4 4-4 4') && d.includes('l-4-4 4-4'))) { - isLoopIcon = true - console.log(`检测到循环图标路径: ${d}`) - break - } - } - - if (isLoopIcon) { - console.log('在主SVG中检测到循环图标,正在转换为文本') - - // 获取图标的位置和尺寸信息 - const width = parseFloat(iconSvg.getAttribute('width') || '18') - const height = parseFloat(iconSvg.getAttribute('height') || '18') - const transform = iconSvg.getAttribute('transform') || '' - - console.log(`图标尺寸: ${width}x${height}, transform: ${transform}`) - - // 尝试从父元素的样式或属性获取位置 - let x = 0, y = 0 - - // 检查transform属性 - const translateMatch = transform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/) - if (translateMatch) { - x = parseFloat(translateMatch[1]) - y = parseFloat(translateMatch[2]) - console.log(`从transform获取位置: (${x}, ${y})`) - } else { - // 尝试从父元素获取位置信息 - const parent = iconSvg.parentElement - if (parent) { - const style = window.getComputedStyle(parent) - const left = parseFloat(style.left || '0') - const top = parseFloat(style.top || '0') - if (left || top) { - x = left - y = top - console.log(`从父元素样式获取位置: (${x}, ${y})`) - } - } - } - - // 创建文本元素替换图标 - const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text') - textElement.setAttribute('x', (x + width / 2).toString()) - textElement.setAttribute('y', (y + height / 2 + 4).toString()) - textElement.setAttribute('text-anchor', 'middle') - textElement.setAttribute('dominant-baseline', 'middle') - textElement.setAttribute('font-family', 'ui-monospace, monospace') - textElement.setAttribute('font-size', Math.min(width * 0.8, 14).toString()) - textElement.setAttribute('fill', 'currentColor') - textElement.textContent = '↻' - - console.log(`创建循环文本元素,位置: (${x + width / 2}, ${y + height / 2 + 4})`) - - // 替换图标SVG - if (iconSvg.parentNode) { - iconSvg.parentNode.replaceChild(textElement, iconSvg) - console.log('成功替换循环图标为文本') - } - } - }) + // Set SVG xmlns attributes to ensure independence + clonedSvg.setAttribute('xmlns', EXPORT_CONFIG.SVG_NAMESPACE) + clonedSvg.setAttribute('xmlns:xlink', EXPORT_CONFIG.XLINK_NAMESPACE) - // 获取计算样式并内联到SVG中 + + // Get computed styles and inline them into SVG const styleElement = document.createElementNS('http://www.w3.org/2000/svg', 'style') const computedStyles = getComputedStylesForSVG(svgElement) styleElement.textContent = computedStyles clonedSvg.insertBefore(styleElement, clonedSvg.firstChild) - // 序列化SVG + // Serialize SVG const serializer = new XMLSerializer() - const svgString = serializer.serializeToString(clonedSvg) + let svgString = serializer.serializeToString(clonedSvg) - // 创建Blob并下载 + // Replace icon placeholders with actual SVG elements using proper SVG positioning + svgString = svgString.replace(/{{INFINITY_ICON}}/g, + ``+ + ``+ + `` + ) + + svgString = svgString.replace(/{{QUANTIFIER_ICON}}/g, + ``+ + EXPORT_CONFIG.SVG_PATHS.QUANTIFIER_PATHS.map(path => ``).join('') + + `` + ) + + // Create Blob and download const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }) downloadBlob(blob, `${filename}.svg`) } catch (error) { - console.error('SVG导出失败:', error) - throw new Error('SVG导出失败') + console.error('SVG export failed:', error) + throw new Error('SVG export failed') } } @@ -161,107 +77,58 @@ export const exportSVG = (svgElement: SVGElement, filename: string = 'regex-grap + + + + /** - * 智能提取文本内容,包括处理图标元素 - * @param element - 要提取文本的DOM元素 - * @returns 提取的文本内容 + * Extract text content from element and replace icons with placeholders + * @param element - DOM element to extract text from + * @returns Extracted text content with icon placeholders */ function extractTextWithIcons(element: Element): string { - console.log('=== extractTextWithIcons 开始处理 ===') - console.log('处理元素:', element) - console.log('子节点数量:', element.childNodes.length) + let content = '' - let result = '' - - // 遍历所有子节点 - for (let i = 0; i < element.childNodes.length; i++) { - const node = element.childNodes[i] - console.log(`处理子节点 ${i}:`, node.nodeType, node) - - if (node.nodeType === Node.TEXT_NODE) { - // 文本节点直接添加 - const textContent = node.textContent || '' - console.log('文本节点内容:', textContent) - result += textContent - } else if (node.nodeType === Node.ELEMENT_NODE) { + // Process child nodes to identify icons and text + Array.from(element.childNodes).forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { const el = node as Element - console.log('元素节点标签:', el.tagName) - - // 检查是否是SVG图标(无穷符号或循环图标) - if (el.tagName.toLowerCase() === 'svg' && el.querySelector('path')) { - // 检查SVG是否包含特定图标的路径特征 - const paths = el.querySelectorAll('path') - let isInfinityIcon = false - let isLoopIcon = false - - for (let j = 0; j < paths.length; j++) { - const path = paths[j] - const d = path.getAttribute('d') || '' - - // 检测无穷符号:检查特定的路径模式 - // 无穷符号通常包含弧形路径和特定的坐标模式 - if ((d.includes('M248,128') && d.includes('95.6,39.6')) || - (d.includes('C') && d.includes('a56,56') && d.length > 100) || - (d.includes('a40,40') && d.includes('56.9') && d.length > 50)) { - isInfinityIcon = true - break - } - - // 检测循环图标:检查循环箭头的特定路径模式 - // 循环图标包含箭头路径和弧形连接线 - if ((d.includes('M17 1l4 4-4 4') || d.includes('M7 23l-4-4 4-4')) || - (d.includes('M3 11V9a4 4') && d.includes('M21 13v2a4 4')) || - (d.includes('l4 4-4 4') && d.includes('l-4-4 4-4'))) { - isLoopIcon = true - break - } - } - - if (isInfinityIcon) { - console.log('检测到无穷符号图标') - result += '∞' - } else if (isLoopIcon) { - console.log('检测到循环图标') - result += '↻' // 使用循环符号 - } else { - console.log('未识别的SVG图标,路径:', paths.length > 0 ? paths[0].getAttribute('d') : 'no paths') - // 其他SVG图标,尝试从aria-label或title获取文本 - const ariaLabel = el.getAttribute('aria-label') - const title = el.querySelector('title')?.textContent - if (ariaLabel) { - result += ariaLabel - } else if (title) { - result += title - } + if (el.tagName.toLowerCase() === 'svg') { + const viewBox = el.getAttribute('viewBox') + if (viewBox === '0 0 256 256') { + content += '{{INFINITY_ICON}}' + } else if (viewBox === '0 0 24 24') { + content += '{{QUANTIFIER_ICON}}' } } else { - // 递归处理其他元素 - result += extractTextWithIcons(el) + content += el.textContent || '' } + } else if (node.nodeType === Node.TEXT_NODE) { + content += node.textContent || '' } - } + }) - return result + return content } /** - * 获取当前主题的颜色值 - * @returns 主题颜色对象 + * Get current theme color values + * @returns Theme color object */ function getCurrentThemeColors() { const rootStyles = getComputedStyle(document.documentElement) - // 获取CSS变量值 - const graphColor = rootStyles.getPropertyValue('--graph').trim() || '#000000' + // Get CSS variable values + const graphColor = rootStyles.getPropertyValue('--graph').trim() || EXPORT_CONFIG.DEFAULT_COLORS.BLACK const foregroundColor = rootStyles.getPropertyValue('--foreground').trim() const backgroundColor = rootStyles.getPropertyValue('--background').trim() - // 转换HSL到十六进制(如果需要) + // Convert HSL to hex (if needed) const convertHslToHex = (hsl: string): string => { if (hsl.startsWith('#')) return hsl - if (!hsl) return '#000000' + if (!hsl) return EXPORT_CONFIG.DEFAULT_COLORS.BLACK - // 简单的HSL到RGB转换(适用于常见的HSL格式) + // Simple HSL to RGB conversion (for common HSL formats) const hslMatch = hsl.match(/([\d.]+)\s+([\d.]+)%\s+([\d.]+)%/) if (hslMatch) { const h = parseFloat(hslMatch[1]) / 360 @@ -286,51 +153,51 @@ function getCurrentThemeColors() { return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` } - return '#000000' + return EXPORT_CONFIG.DEFAULT_COLORS.BLACK } return { - graph: graphColor.startsWith('#') ? graphColor : convertHslToHex(foregroundColor) || '#000000', - foreground: convertHslToHex(foregroundColor) || '#000000', - background: convertHslToHex(backgroundColor) || '#ffffff' + graph: graphColor.startsWith('#') ? graphColor : convertHslToHex(foregroundColor) || EXPORT_CONFIG.DEFAULT_COLORS.BLACK, + foreground: convertHslToHex(foregroundColor) || EXPORT_CONFIG.DEFAULT_COLORS.BLACK, + background: convertHslToHex(backgroundColor) || EXPORT_CONFIG.DEFAULT_COLORS.WHITE } } /** - * 获取SVG的计算样式 - * @param svgElement SVG元素 - * @returns CSS样式字符串 + * Get computed styles for SVG + * @param svgElement SVG element + * @returns CSS style string */ function getComputedStylesForSVG(svgElement: SVGElement): string { const colors = getCurrentThemeColors() const styles: string[] = [] - // 添加基础样式,使用黑色作为导出颜色 + // Add basic styles, use black as export color styles.push(` - .stroke-graph { stroke: #000000 !important; stroke-width: 1; } + .stroke-graph { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; stroke-width: 1; } .fill-transparent { fill: transparent !important; } - .text-foreground { fill: #000000 !important; } + .text-foreground { fill: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } .rounded-lg { rx: 8; ry: 8; } - .border { stroke: #000000 !important; stroke-width: 1; } + .border { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; stroke-width: 1; } .font-mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace; } .text-center { text-anchor: middle; } .whitespace-nowrap { white-space: nowrap; } .leading-normal { line-height: 1.5; } .pointer-events-none { pointer-events: none; } - text { fill: #000000 !important; } - path { stroke: #000000 !important; } - rect { stroke: #000000 !important; } - circle { stroke: #000000 !important; } - line { stroke: #000000 !important; } + text { fill: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; dominant-baseline: central; alignment-baseline: middle; } + path { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + rect { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + circle { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } + line { stroke: ${EXPORT_CONFIG.DEFAULT_COLORS.BLACK} !important; } `) return styles.join('\n') } /** - * 下载Blob文件 - * @param blob Blob对象 - * @param filename 文件名 + * Download Blob file + * @param blob Blob object + * @param filename File name */ function downloadBlob(blob: Blob, filename: string) { const url = URL.createObjectURL(blob) @@ -344,16 +211,16 @@ function downloadBlob(blob: Blob, filename: string) { } /** - * 统一的导出函数 - * @param format 导出格式 - * @param element 要导出的元素 - * @param filename 文件名 - * @param options 导出选项 + * Unified export function + * @param format Export format + * @param element Element to export + * @param filename File name + * @param options Export options */ export const exportGraph = async ( format: ExportFormat, element: HTMLElement | SVGElement, - filename: string = 'regex-graph', + filename: string = EXPORT_CONFIG.DEFAULT_FILENAME, options: any = {} ) => { switch (format) { @@ -361,16 +228,16 @@ export const exportGraph = async ( if (element instanceof SVGElement) { exportSVG(element, filename) } else { - // 如果传入的不是SVG元素,尝试查找SVG子元素 + // If the passed element is not an SVG element, try to find SVG child element const svgElement = element.querySelector('svg') if (svgElement) { exportSVG(svgElement, filename) } else { - throw new Error('未找到SVG元素') + throw new Error('SVG element not found') } } break default: - throw new Error(`不支持的导出格式: ${format}`) + throw new Error(`Unsupported export format: ${format}`) } } \ No newline at end of file diff --git a/src/modules/home/index.tsx b/src/modules/home/index.tsx index b29c8a6..823dc7e 100644 --- a/src/modules/home/index.tsx +++ b/src/modules/home/index.tsx @@ -132,8 +132,8 @@ function Home() { const handleExport = async (format: ExportFormat) => { try { - // 查找图形容器元素 - // 获取SVG的父容器,而不是SVG元素本身 + // Find graph container element + // Get the parent container of SVG, not the SVG element itself const svgElement = document.querySelector('[data-testid="graph"]') as SVGElement const graphElement = svgElement?.parentElement || svgElement if (!graphElement) { @@ -144,7 +144,7 @@ function Home() { return } - // 生成文件名 + // Generate filename const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-') const filename = `regex-graph-${timestamp}` @@ -153,7 +153,6 @@ function Home() { description: t(`Graph exported as ${format.toUpperCase()}`) }) } catch (error) { - console.error('Export failed:', error) toast({ description: t('Export failed'), variant: 'destructive' diff --git a/src/modules/samples/data.ts b/src/modules/samples/data.ts index 89b3f34..676cdaa 100644 --- a/src/modules/samples/data.ts +++ b/src/modules/samples/data.ts @@ -1,4 +1,4 @@ -// 正则表达式样例数据 +// Regex sample data export interface RegexSample { desc: string label: string @@ -13,7 +13,7 @@ export interface RegexCategory { samples: RegexSample[] } -// 正则表达式分类数据 +// Regex category data export const regexCategories: RegexCategory[] = [ { id: 'numbers', @@ -24,37 +24,37 @@ export const regexCategories: RegexCategory[] = [ desc: 'Whole Numbers', label: '/^\\d+$/', regex: '^\\d+$', - explanation: '匹配整数' + explanation: 'Matches one or more consecutive digits from start to end of string' }, { desc: 'Decimal Numbers', label: '/^\\d*\\.\\d+$/', regex: '^\\d*\\.\\d+$', - explanation: '匹配小数' + explanation: 'Matches numbers with decimal point, allowing optional digits before decimal' }, { desc: 'Whole + Decimal Numbers', label: '/^\\d*(\\.\\d+)?$/', regex: '^\\d*(\\.\\d+)?$', - explanation: '匹配整数和小数' + explanation: 'Matches both integers and decimals using optional decimal group' }, { desc: 'Negative, Positive Whole + Decimal Numbers', label: '/^-?\\d*(\\.\\d+)?$/', regex: '^-?\\d*(\\.\\d+)?$', - explanation: '匹配正负整数和小数' + explanation: 'Includes optional minus sign for negative numbers with decimal support' }, { desc: 'Currency Amount', label: '/^\\$?\\d{1,3}(,\\d{3})*(\\.\\d{2})?$/', regex: '^\\$?\\d{1,3}(,\\d{3})*(\\.\\d{2})?$', - explanation: '匹配货币金额格式' + explanation: 'Validates currency format with optional dollar sign, comma separators, and cents' }, { desc: 'Percentage', label: '/^\\d{1,3}(\\.\\d{1,2})?%$/', regex: '^\\d{1,3}(\\.\\d{1,2})?%$', - explanation: '匹配百分比' + explanation: 'Matches percentage values from 0-999% with up to 2 decimal places' } ] }, @@ -67,25 +67,25 @@ export const regexCategories: RegexCategory[] = [ desc: 'Basic URL', label: '/^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$/', regex: '^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)$', - explanation: '匹配基本的HTTP/HTTPS URL' + explanation: 'Validates HTTP/HTTPS URLs with optional www prefix and query parameters' }, { desc: 'Domain Name', label: '/^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$/', regex: '^[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$', - explanation: '匹配域名' + explanation: 'Matches valid domain names following RFC standards with length limits' }, { desc: 'FTP URL', label: '/^ftp:\\/\\/[\\w\\.-]+\\.[a-zA-Z]{2,}(:\\d+)?(\\/.*)?$/', regex: '^ftp:\\/\\/[\\w\\.-]+\\.[a-zA-Z]{2,}(:\\d+)?(\\/.*)?$', - explanation: '匹配FTP URL' + explanation: 'Validates FTP protocol URLs with optional port and path components' }, { desc: 'URL with Port', label: '/^https?:\\/\\/[\\w\\.-]+(:\\d+)?(\\/.*)?$/', regex: '^https?:\\/\\/[\\w\\.-]+(:\\d+)?(\\/.*)?$', - explanation: '匹配带端口号的URL' + explanation: 'Matches URLs with optional port numbers and path segments' } ] }, @@ -98,31 +98,31 @@ export const regexCategories: RegexCategory[] = [ desc: 'Date Format YYYY-MM-DD', label: '/^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/', regex: '^[12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$', - explanation: '匹配YYYY-MM-DD格式日期' + explanation: 'Validates ISO date format with proper month and day ranges' }, { desc: 'Date Format DD/MM/YYYY', label: '/^(0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/[12]\\d{3}$/', regex: '^(0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/[12]\\d{3}$', - explanation: '匹配DD/MM/YYYY格式日期' + explanation: 'European date format with day-first ordering and slash separators' }, { desc: 'Date Format MM/DD/YYYY', label: '/^(0[1-9]|1[0-2])\\/(0[1-9]|[12]\\d|3[01])\\/[12]\\d{3}$/', regex: '^(0[1-9]|1[0-2])\\/(0[1-9]|[12]\\d|3[01])\\/[12]\\d{3}$', - explanation: '匹配MM/DD/YYYY格式日期' + explanation: 'American date format with month-first ordering and validation' }, { desc: 'Time Format HH:MM', label: '/^([01]?\\d|2[0-3]):[0-5]\\d$/', regex: '^([01]?\\d|2[0-3]):[0-5]\\d$', - explanation: '匹配24小时制时间格式' + explanation: 'Validates 24-hour time format with proper hour and minute ranges' }, { desc: 'DateTime ISO 8601', label: '/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$/', regex: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$', - explanation: '匹配ISO 8601日期时间格式' + explanation: 'Matches ISO 8601 datetime with optional milliseconds and timezone' } ] }, @@ -135,25 +135,25 @@ export const regexCategories: RegexCategory[] = [ desc: 'Chinese Mobile Phone', label: '/^1[3-9]\\d{9}$/', regex: '^1[3-9]\\d{9}$', - explanation: '匹配中国大陆手机号码' + explanation: 'Validates 11-digit Chinese mobile numbers starting with 13-19' }, { desc: 'US Phone Number', label: '/^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/', regex: '^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$', - explanation: '匹配美国电话号码格式' + explanation: 'Matches US phone format with optional parentheses and separators' }, { desc: 'International Phone', label: '/^\\+?[1-9]\\d{1,14}$/', regex: '^\\+?[1-9]\\d{1,14}$', - explanation: '匹配国际电话号码格式' + explanation: 'Validates international phone numbers following E.164 standard' }, { desc: 'Chinese Landline', label: '/^0\\d{2,3}-?\\d{7,8}$/', regex: '^0\\d{2,3}-?\\d{7,8}$', - explanation: '匹配中国固定电话号码' + explanation: 'Matches Chinese landline format with area code and optional dash' } ] }, @@ -166,19 +166,19 @@ export const regexCategories: RegexCategory[] = [ desc: 'IPv4 Address', label: '/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/', regex: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', - explanation: '匹配IPv4地址' + explanation: 'Validates IPv4 addresses with proper octet range validation (0-255)' }, { desc: 'IPv6 Address', label: '/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/', regex: '^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$', - explanation: '匹配完整IPv6地址' + explanation: 'Matches full IPv6 addresses with 8 hexadecimal groups separated by colons' }, { desc: 'MAC Address', label: '/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/', regex: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', - explanation: '匹配MAC地址' + explanation: 'Validates MAC addresses with colon or hyphen separators between hex pairs' } ] }, @@ -191,25 +191,25 @@ export const regexCategories: RegexCategory[] = [ desc: 'HTML Tag', label: '/<\\/?[a-zA-Z][a-zA-Z0-9]*[^<>]*>/', regex: '<\\/?[a-zA-Z][a-zA-Z0-9]*[^<>]*>', - explanation: '匹配HTML标签' + explanation: 'Matches opening and closing HTML tags with optional attributes' }, { desc: 'HTML Tag with Attributes', label: '/<([a-zA-Z][a-zA-Z0-9]*)\\b[^>]*>(.*?)<\\/\\1>/', regex: '<([a-zA-Z][a-zA-Z0-9]*)\\b[^>]*>(.*?)<\\/\\1>', - explanation: '匹配带属性的HTML标签对' + explanation: 'Captures complete HTML tag pairs with content using backreferences' }, { desc: 'HTML Comment', label: '//', regex: '', - explanation: '匹配HTML注释' + explanation: 'Matches HTML comments including multiline content with non-greedy matching' }, { desc: 'HTML Image Tag', label: '/]*src\\s*=\\s*["\']([^"\'>]+)["\'][^>]*>/', regex: ']*src\\s*=\\s*["\']([^"\'>]+)["\'][^>]*>', - explanation: '匹配HTML图片标签并提取src属性' + explanation: 'Extracts src attribute value from img tags with flexible attribute ordering' } ] }, @@ -222,13 +222,13 @@ export const regexCategories: RegexCategory[] = [ desc: 'Basic Email', label: '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/', regex: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', - explanation: '匹配基本邮箱格式' + explanation: 'Validates common email format with alphanumeric characters and standard symbols' }, { desc: 'Strict Email RFC 5322', label: '/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/', regex: '^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$', - explanation: '严格的RFC 5322邮箱格式' + explanation: 'Comprehensive RFC 5322 compliant email validation with full character set support' } ] }, @@ -241,13 +241,13 @@ export const regexCategories: RegexCategory[] = [ desc: 'Strong Password', label: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$/', regex: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$', - explanation: '强密码:至少8位,包含大小写字母、数字和特殊字符' + explanation: 'Strong password validation using positive lookaheads for complexity requirements' }, { desc: 'Medium Password', label: '/^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{6,}$/', regex: '^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{6,}$', - explanation: '中等强度密码:至少6位,包含字母和数字' + explanation: 'Medium strength password requiring letters and numbers with minimum length' } ] }, @@ -260,30 +260,30 @@ export const regexCategories: RegexCategory[] = [ desc: 'Chinese ID Card', label: '/^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$/', regex: '^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$', - explanation: '匹配中国身份证号码' + explanation: 'Validates Chinese national ID format with birth date and checksum validation' }, { desc: 'Credit Card Number', label: '/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$/', regex: '^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$', - explanation: '匹配信用卡号码(Visa、MasterCard、AmEx等)' + explanation: 'Matches major credit card formats including Visa, MasterCard, AmEx, and Discover' } ] } ] -// 获取所有样例的扁平化列表 +// Get flattened list of all samples export function getAllSamples(): RegexSample[] { return regexCategories.flatMap(category => category.samples) } -// 根据分类ID获取样例 +// Get samples by category ID export function getSamplesByCategory(categoryId: string): RegexSample[] { const category = regexCategories.find(cat => cat.id === categoryId) return category ? category.samples : [] } -// 获取分类信息 +// Get category information export function getCategoryById(categoryId: string): RegexCategory | undefined { return regexCategories.find(cat => cat.id === categoryId) } \ No newline at end of file diff --git a/src/modules/samples/index.tsx b/src/modules/samples/index.tsx index 3219bd7..286d3d0 100644 --- a/src/modules/samples/index.tsx +++ b/src/modules/samples/index.tsx @@ -1,7 +1,6 @@ import { useState } from 'react' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import clsx from 'clsx' import SimpleGraph from '@/modules/graph/simple-graph' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { Button } from '@/components/ui/button' @@ -12,109 +11,92 @@ function Samples() { const { t } = useTranslation() const [selectedCategory, setSelectedCategory] = useState('all') - // 获取当前选中分类的样例 + // Get samples for currently selected category const currentSamples: RegexSample[] = selectedCategory === 'all' ? getAllSamples() : getSamplesByCategory(selectedCategory) return (
- {/* 左侧分类栏 */} + {/* Left sidebar */}
-

- {t('Samples')} +

+ Regex Samples

- {/* 全部分类按钮 */} + {/* All categories button */} - {/* 分类按钮 */} + {/* Category buttons */} {regexCategories.map((category) => ( ))}
- {/* 右侧内容区域 */} + {/* Main content area */}
-
+
- {/* 分类标题 */} -
-

- {selectedCategory === 'all' - ? t('All Categories') - : t(regexCategories.find(cat => cat.id === selectedCategory)?.name || '') - } -

-

- {selectedCategory === 'all' - ? `共 ${currentSamples.length} 个正则表达式样例` - : `${currentSamples.length} 个样例` - } -

-
+ {/* Category header */} + - {/* 样例列表 */} + {/* Sample list */}
{currentSamples.map((sample, index) => { const linkTo = `/?r=${encodeURIComponent(`/${sample.regex}/`)}` return ( -
- {/* 样例标题和描述 */} +
+ {/* Sample title and description */}
-

- {t(sample.desc)} +

+ {sample.desc}

-

+

{sample.explanation}

- {/* 正则表达式 */} -
+ {/* Regular expression */} +
- + {sample.label}
- {/* 可视化图形 */} -
+ {/* Visualization graph */} +
-
+
@@ -126,12 +108,12 @@ function Samples() { })}
- {/* 空状态 */} + {/* Empty state */} {currentSamples.length === 0 && (
📝

- 该分类暂无样例 + No regex samples available for this category

)} From f303648a56afa93c0b36fcc6e0ab73875882612e Mon Sep 17 00:00:00 2001 From: persist-1 Date: Thu, 21 Aug 2025 19:57:14 +0800 Subject: [PATCH 05/14] fix: - Fixed the style failure of special ICONS when exporting svg (by re-manipulating the DOM to set the size before serialization) --- src/modules/export/utils.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/modules/export/utils.ts b/src/modules/export/utils.ts index bf28663..78e7043 100644 --- a/src/modules/export/utils.ts +++ b/src/modules/export/utils.ts @@ -40,6 +40,18 @@ export const exportSVG = (svgElement: SVGElement, filename: string = EXPORT_CONF clonedSvg.setAttribute('xmlns', EXPORT_CONFIG.SVG_NAMESPACE) clonedSvg.setAttribute('xmlns:xlink', EXPORT_CONFIG.XLINK_NAMESPACE) + // Modify icon dimensions before serialization + const quantifierIcons = clonedSvg.querySelectorAll('svg[viewBox="0 0 24 24"]') + quantifierIcons.forEach(icon => { + icon.setAttribute('width', '18') + icon.setAttribute('height', '10') + }) + + const infinityIcons = clonedSvg.querySelectorAll('svg[viewBox="0 0 256 256"]') + infinityIcons.forEach(icon => { + icon.setAttribute('width', '18') + icon.setAttribute('height', '10') + }) // Get computed styles and inline them into SVG const styleElement = document.createElementNS('http://www.w3.org/2000/svg', 'style') @@ -51,15 +63,15 @@ export const exportSVG = (svgElement: SVGElement, filename: string = EXPORT_CONF const serializer = new XMLSerializer() let svgString = serializer.serializeToString(clonedSvg) - // Replace icon placeholders with actual SVG elements using proper SVG positioning + // Replace icon placeholders with actual SVG elements svgString = svgString.replace(/{{INFINITY_ICON}}/g, - ``+ + ``+ ``+ `` ) svgString = svgString.replace(/{{QUANTIFIER_ICON}}/g, - ``+ + ``+ EXPORT_CONFIG.SVG_PATHS.QUANTIFIER_PATHS.map(path => ``).join('') + `` ) From f4eed43ca538f73d114e437899f1fc134cde147c Mon Sep 17 00:00:00 2001 From: persist-1 Date: Thu, 21 Aug 2025 20:06:10 +0800 Subject: [PATCH 06/14] fix: - Fix a useless and stupid addition - remove the unnecessary English to English translation (due to my lax review of the ai code, it wrote some strange things...) --- public/locales/en/translation.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 public/locales/en/translation.json diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json deleted file mode 100644 index 72ce710..0000000 --- a/public/locales/en/translation.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Export": "Export", - "Export as SVG": "Export as SVG", - "No graph to export": "No graph to export", - "Export failed": "Export failed", - "Graph exported as SVG": "Graph exported as SVG" -} \ No newline at end of file From 8a2fb2cbf76619c9dfad5c903a164381134e3277 Mon Sep 17 00:00:00 2001 From: persist-1 Date: Thu, 21 Aug 2025 22:11:03 +0800 Subject: [PATCH 07/14] update: - updated translation about samples page - updated style about samples page --- .gitignore | 14 ++++++++- public/locales/cn/translation.json | 36 ++++++++++++++++++++++- public/locales/jp/translation.json | 46 +++++++++++++++++++++++++++++- public/locales/ru/translation.json | 46 +++++++++++++++++++++++++++++- src/global.css | 2 +- src/modules/samples/index.tsx | 28 +++++++++++------- 6 files changed, 157 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 30365a2..c6ad3fa 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,16 @@ yarn-error.log* /export -.swc \ No newline at end of file +.swc + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +.tests-examples/ +.playwright.config.ts +.reference/ +.rules +.debug_tools/ diff --git a/public/locales/cn/translation.json b/public/locales/cn/translation.json index 273483b..784364b 100644 --- a/public/locales/cn/translation.json +++ b/public/locales/cn/translation.json @@ -160,5 +160,39 @@ "Copy permalink": "复制链接", "Permalink copied.": "链接已复制", "Empty": "空", - "Group's name": "组名" + "Group's name": "组名", + "Regex Samples": "正则表达式样例", + "No regex samples available for this category": "此分类下没有可用的正则表达式样例", + "Matches one or more consecutive digits from start to end of string": "从字符串开始到结束匹配一个或多个连续数字", + "Matches numbers with decimal point, allowing optional digits before decimal": "匹配带小数点的数字,小数点前可选数字", + "Matches both integers and decimals using optional decimal group": "使用可选小数组匹配整数和小数", + "Includes optional minus sign for negative numbers with decimal support": "包含可选负号,支持负数和小数", + "Validates currency format with optional dollar sign, comma separators, and cents": "验证货币格式,可选美元符号、逗号分隔符和分", + "Matches percentage values from 0-999% with up to 2 decimal places": "匹配0-999%的百分比值,最多2位小数", + "Validates HTTP/HTTPS URLs with optional www prefix and query parameters": "验证HTTP/HTTPS URL,可选www前缀和查询参数", + "Matches valid domain names following RFC standards with length limits": "匹配符合RFC标准的有效域名,有长度限制", + "Validates FTP protocol URLs with optional port and path components": "验证FTP协议URL,可选端口和路径组件", + "Matches URLs with optional port numbers and path segments": "匹配带可选端口号和路径段的URL", + "Validates ISO date format with proper month and day ranges": "验证ISO日期格式,正确的月份和日期范围", + "European date format with day-first ordering and slash separators": "欧洲日期格式,日期在前,斜杠分隔", + "American date format with month-first ordering and validation": "美国日期格式,月份在前并验证", + "Validates 24-hour time format with proper hour and minute ranges": "验证24小时时间格式,正确的小时和分钟范围", + "Matches ISO 8601 datetime with optional milliseconds and timezone": "匹配ISO 8601日期时间,可选毫秒和时区", + "Validates 11-digit Chinese mobile numbers starting with 13-19": "验证以13-19开头的11位中国手机号", + "Matches US phone format with optional parentheses and separators": "匹配美国电话格式,可选括号和分隔符", + "Validates international phone numbers following E.164 standard": "验证符合E.164标准的国际电话号码", + "Matches Chinese landline format with area code and optional dash": "匹配中国固定电话格式,带区号和可选破折号", + "Validates IPv4 addresses with proper octet range validation (0-255)": "验证IPv4地址,正确的八位字节范围验证(0-255)", + "Matches full IPv6 addresses with 8 hexadecimal groups separated by colons": "匹配完整IPv6地址,8个十六进制组用冒号分隔", + "Validates MAC addresses with colon or hyphen separators between hex pairs": "验证MAC地址,十六进制对之间用冒号或连字符分隔", + "Matches opening and closing HTML tags with optional attributes": "匹配开始和结束HTML标签,可选属性", + "Captures complete HTML tag pairs with content using backreferences": "使用反向引用捕获完整的HTML标签对及内容", + "Matches HTML comments including multiline content with non-greedy matching": "匹配HTML注释,包括多行内容,非贪婪匹配", + "Extracts src attribute value from img tags with flexible attribute ordering": "从img标签中提取src属性值,灵活的属性顺序", + "Validates common email format with alphanumeric characters and standard symbols": "验证常见邮箱格式,字母数字字符和标准符号", + "Comprehensive RFC 5322 compliant email validation with full character set support": "全面的RFC 5322兼容邮箱验证,支持完整字符集", + "Strong password validation using positive lookaheads for complexity requirements": "使用正向前瞻的强密码验证,满足复杂性要求", + "Medium strength password requiring letters and numbers with minimum length": "中等强度密码,需要字母和数字,最小长度", + "Validates Chinese national ID format with birth date and checksum validation": "验证中国身份证格式,包含出生日期和校验位验证", + "Matches major credit card formats including Visa, MasterCard, AmEx, and Discover": "匹配主要信用卡格式,包括Visa、万事达、美国运通和Discover" } diff --git a/public/locales/jp/translation.json b/public/locales/jp/translation.json index 185ac2c..e0c4183 100644 --- a/public/locales/jp/translation.json +++ b/public/locales/jp/translation.json @@ -164,5 +164,49 @@ "Copy permalink": "パーマリンクをコピー", "Permalink copied.": "パーマリンクがコピーされました", "Empty": "空", - "Group's name": "グループ名" + "Group's name": "グループ名", + "Regex Samples": "正規表現サンプル", + "All Categories": "全カテゴリ", + "Numbers": "数字", + "URLs": "URL", + "Dates": "日付", + "Phone Numbers": "電話番号", + "IP Addresses": "IPアドレス", + "HTML Tags": "HTMLタグ", + "Email Addresses": "メールアドレス", + "Passwords": "パスワード", + "ID Numbers": "ID番号", + "No regex samples available for this category": "このカテゴリには利用可能な正規表現サンプルがありません", + "Matches one or more consecutive digits from start to end of string": "文字列の開始から終了まで1つ以上の連続する数字にマッチ", + "Matches numbers with decimal point, allowing optional digits before decimal": "小数点のある数字にマッチ、小数点前の数字は任意", + "Matches both integers and decimals using optional decimal group": "オプションの小数グループを使用して整数と小数の両方にマッチ", + "Includes optional minus sign for negative numbers with decimal support": "負の数のオプションのマイナス記号を含み、小数をサポート", + "Validates currency format with optional dollar sign, comma separators, and cents": "オプションのドル記号、カンマ区切り、セントを含む通貨形式を検証", + "Matches percentage values from 0-999% with up to 2 decimal places": "最大2桁の小数点以下を持つ0-999%のパーセンテージ値にマッチ", + "Validates HTTP/HTTPS URLs with optional www prefix and query parameters": "オプションのwwwプレフィックスとクエリパラメータを持つHTTP/HTTPS URLを検証", + "Matches valid domain names following RFC standards with length limits": "長さ制限のあるRFC標準に従った有効なドメイン名にマッチ", + "Validates FTP protocol URLs with optional port and path components": "オプションのポートとパスコンポーネントを持つFTPプロトコルURLを検証", + "Matches URLs with optional port numbers and path segments": "オプションのポート番号とパスセグメントを持つURLにマッチ", + "Validates ISO date format with proper month and day ranges": "適切な月と日の範囲を持つISO日付形式を検証", + "European date format with day-first ordering and slash separators": "日付優先順序とスラッシュ区切りのヨーロッパ日付形式", + "American date format with month-first ordering and validation": "月優先順序と検証のアメリカ日付形式", + "Validates 24-hour time format with proper hour and minute ranges": "適切な時間と分の範囲を持つ24時間時刻形式を検証", + "Matches ISO 8601 datetime with optional milliseconds and timezone": "オプションのミリ秒とタイムゾーンを持つISO 8601日時にマッチ", + "Validates 11-digit Chinese mobile numbers starting with 13-19": "13-19で始まる11桁の中国携帯電話番号を検証", + "Matches US phone format with optional parentheses and separators": "オプションの括弧と区切り文字を持つアメリカ電話形式にマッチ", + "Validates international phone numbers following E.164 standard": "E.164標準に従った国際電話番号を検証", + "Matches Chinese landline format with area code and optional dash": "市外局番とオプションのダッシュを持つ中国固定電話形式にマッチ", + "Validates IPv4 addresses with proper octet range validation (0-255)": "適切なオクテット範囲検証(0-255)を持つIPv4アドレスを検証", + "Matches full IPv6 addresses with 8 hexadecimal groups separated by colons": "コロンで区切られた8つの16進グループを持つ完全なIPv6アドレスにマッチ", + "Validates MAC addresses with colon or hyphen separators between hex pairs": "16進ペア間のコロンまたはハイフン区切りを持つMACアドレスを検証", + "Matches opening and closing HTML tags with optional attributes": "オプションの属性を持つ開始と終了HTMLタグにマッチ", + "Captures complete HTML tag pairs with content using backreferences": "後方参照を使用してコンテンツを持つ完全なHTMLタグペアをキャプチャ", + "Matches HTML comments including multiline content with non-greedy matching": "非貪欲マッチングで複数行コンテンツを含むHTMLコメントにマッチ", + "Extracts src attribute value from img tags with flexible attribute ordering": "柔軟な属性順序でimgタグからsrc属性値を抽出", + "Validates common email format with alphanumeric characters and standard symbols": "英数字と標準記号を持つ一般的なメール形式を検証", + "Comprehensive RFC 5322 compliant email validation with full character set support": "完全な文字セットサポートを持つ包括的なRFC 5322準拠メール検証", + "Strong password validation using positive lookaheads for complexity requirements": "複雑性要件のための正の先読みを使用した強力なパスワード検証", + "Medium strength password requiring letters and numbers with minimum length": "最小長を持つ文字と数字を必要とする中程度の強度のパスワード", + "Validates Chinese national ID format with birth date and checksum validation": "生年月日とチェックサム検証を持つ中国国民ID形式を検証", + "Matches major credit card formats including Visa, MasterCard, AmEx, and Discover": "Visa、MasterCard、AmEx、Discoverを含む主要なクレジットカード形式にマッチ" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 14aa280..ea07665 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -163,5 +163,49 @@ "Copy permalink": "Скопировать ссылку", "Permalink copied.": "Ссылка скопирована", "Empty": "Пустой узел", - "Group's name": "Имя группы" + "Group's name": "Имя группы", + "Regex Samples": "Примеры регулярных выражений", + "All Categories": "Все категории", + "Numbers": "Числа", + "URLs": "URL-адреса", + "Dates": "Даты", + "Phone Numbers": "Телефонные номера", + "IP Addresses": "IP-адреса", + "HTML Tags": "HTML-теги", + "Email Addresses": "Адреса электронной почты", + "Passwords": "Пароли", + "ID Numbers": "Номера удостоверений", + "No regex samples available for this category": "Нет доступных примеров регулярных выражений для этой категории", + "Matches one or more consecutive digits from start to end of string": "Соответствует одной или нескольким последовательным цифрам от начала до конца строки", + "Matches numbers with decimal point, allowing optional digits before decimal": "Соответствует числам с десятичной точкой, допуская необязательные цифры перед десятичной", + "Matches both integers and decimals using optional decimal group": "Соответствует как целым числам, так и десятичным, используя необязательную десятичную группу", + "Includes optional minus sign for negative numbers with decimal support": "Включает необязательный знак минус для отрицательных чисел с поддержкой десятичных", + "Validates currency format with optional dollar sign, comma separators, and cents": "Проверяет формат валюты с необязательным знаком доллара, запятыми-разделителями и центами", + "Matches percentage values from 0-999% with up to 2 decimal places": "Соответствует процентным значениям от 0-999% с до 2 десятичных знаков", + "Validates HTTP/HTTPS URLs with optional www prefix and query parameters": "Проверяет HTTP/HTTPS URL с необязательным префиксом www и параметрами запроса", + "Matches valid domain names following RFC standards with length limits": "Соответствует действительным доменным именам, следующим стандартам RFC с ограничениями длины", + "Validates FTP protocol URLs with optional port and path components": "Проверяет URL протокола FTP с необязательными компонентами порта и пути", + "Matches URLs with optional port numbers and path segments": "Соответствует URL с необязательными номерами портов и сегментами пути", + "Validates ISO date format with proper month and day ranges": "Проверяет формат даты ISO с правильными диапазонами месяцев и дней", + "European date format with day-first ordering and slash separators": "Европейский формат даты с порядком день-первый и разделителями слэш", + "American date format with month-first ordering and validation": "Американский формат даты с порядком месяц-первый и проверкой", + "Validates 24-hour time format with proper hour and minute ranges": "Проверяет 24-часовой формат времени с правильными диапазонами часов и минут", + "Matches ISO 8601 datetime with optional milliseconds and timezone": "Соответствует дате-времени ISO 8601 с необязательными миллисекундами и часовым поясом", + "Validates 11-digit Chinese mobile numbers starting with 13-19": "Проверяет 11-значные китайские мобильные номера, начинающиеся с 13-19", + "Matches US phone format with optional parentheses and separators": "Соответствует формату телефона США с необязательными скобками и разделителями", + "Validates international phone numbers following E.164 standard": "Проверяет международные телефонные номера, следующие стандарту E.164", + "Matches Chinese landline format with area code and optional dash": "Соответствует формату китайского стационарного телефона с кодом области и необязательным тире", + "Validates IPv4 addresses with proper octet range validation (0-255)": "Проверяет адреса IPv4 с правильной проверкой диапазона октетов (0-255)", + "Matches full IPv6 addresses with 8 hexadecimal groups separated by colons": "Соответствует полным адресам IPv6 с 8 шестнадцатеричными группами, разделенными двоеточиями", + "Validates MAC addresses with colon or hyphen separators between hex pairs": "Проверяет MAC-адреса с разделителями двоеточие или дефис между шестнадцатеричными парами", + "Matches opening and closing HTML tags with optional attributes": "Соответствует открывающим и закрывающим HTML-тегам с необязательными атрибутами", + "Captures complete HTML tag pairs with content using backreferences": "Захватывает полные пары HTML-тегов с содержимым, используя обратные ссылки", + "Matches HTML comments including multiline content with non-greedy matching": "Соответствует HTML-комментариям, включая многострочное содержимое с нежадным сопоставлением", + "Extracts src attribute value from img tags with flexible attribute ordering": "Извлекает значение атрибута src из тегов img с гибким порядком атрибутов", + "Validates common email format with alphanumeric characters and standard symbols": "Проверяет общий формат электронной почты с буквенно-цифровыми символами и стандартными символами", + "Comprehensive RFC 5322 compliant email validation with full character set support": "Всеобъемлющая проверка электронной почты, соответствующая RFC 5322, с поддержкой полного набора символов", + "Strong password validation using positive lookaheads for complexity requirements": "Проверка надежного пароля с использованием положительных просмотров вперед для требований сложности", + "Medium strength password requiring letters and numbers with minimum length": "Пароль средней силы, требующий букв и цифр с минимальной длиной", + "Validates Chinese national ID format with birth date and checksum validation": "Проверяет формат китайского национального удостоверения личности с проверкой даты рождения и контрольной суммы", + "Matches major credit card formats including Visa, MasterCard, AmEx, and Discover": "Соответствует основным форматам кредитных карт, включая Visa, MasterCard, AmEx и Discover" } diff --git a/src/global.css b/src/global.css index e7ae7e4..5711014 100644 --- a/src/global.css +++ b/src/global.css @@ -61,7 +61,7 @@ --chart-5: 340 75% 55%; --graph: #d4d4d8; --graph-group: #52525b; - --graph-bg: #111111; + --graph-bg: #000000; } } diff --git a/src/modules/samples/index.tsx b/src/modules/samples/index.tsx index 286d3d0..382838d 100644 --- a/src/modules/samples/index.tsx +++ b/src/modules/samples/index.tsx @@ -19,21 +19,25 @@ function Samples() { return (
{/* Left sidebar */} -
+

- Regex Samples + {t('Regex Samples')}

{/* All categories button */} @@ -43,12 +47,16 @@ function Samples() { @@ -70,15 +78,15 @@ function Samples() { {currentSamples.map((sample, index) => { const linkTo = `/?r=${encodeURIComponent(`/${sample.regex}/`)}` return ( -
+
{/* Sample title and description */}

- {sample.desc} + {t(sample.desc)}

- {sample.explanation} + {sample.explanation ? t(sample.explanation) : ''}

@@ -113,7 +121,7 @@ function Samples() {
📝

- No regex samples available for this category + {t('No regex samples available for this category')}

)} From 204ef2aec33fc7b52b739361dab7741a11362411 Mon Sep 17 00:00:00 2001 From: persist-1 Date: Thu, 21 Aug 2025 22:44:04 +0800 Subject: [PATCH 08/14] update: - updated style about samples page,make the effect of the sample page close to the overall project's art style --- src/modules/export/utils.ts | 8 -------- src/modules/samples/index.tsx | 20 ++++++-------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/modules/export/utils.ts b/src/modules/export/utils.ts index 78e7043..eb9ab0c 100644 --- a/src/modules/export/utils.ts +++ b/src/modules/export/utils.ts @@ -85,14 +85,6 @@ export const exportSVG = (svgElement: SVGElement, filename: string = EXPORT_CONF } } - - - - - - - - /** * Extract text content from element and replace icons with placeholders * @param element - DOM element to extract text from diff --git a/src/modules/samples/index.tsx b/src/modules/samples/index.tsx index 382838d..b8a707f 100644 --- a/src/modules/samples/index.tsx +++ b/src/modules/samples/index.tsx @@ -25,18 +25,14 @@ function Samples() { {t('Regex Samples')}
- {/* All categories button */} + {/* All samples button */}