diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ff35225..4fbef56 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -102,7 +102,7 @@ pages/FeatureName/ ### 代码分割 -`wxt.config.ts` 通过 `manualChunksForHtmlOnly()` 自动分组 vendor 依赖(vendor-react、vendor-i18n、vendor-qr、vendor-dnd、vendor-markdown),无需手动配置。 +`wxt.config.ts` 通过 `manualChunksForHtmlOnly()` 自动分组 vendor 依赖(vendor-react、vendor-i18n、vendor-qr、vendor-dnd),无需手动配置。 ### 代码风格 diff --git a/AGENTS.md b/AGENTS.md index 568c80b..8a9b20d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -142,7 +142,6 @@ src/pages/FeatureName/ - `@webext-core/messaging` — 扩展消息通信 - `@dnd-kit` — 拖拽排序(用于页面顺序管理) -- `marked` — Markdown 解析 - `qrious` + `qr-scanner` — 二维码生成与解析 - `dayjs` — 日期处理(时间戳转换) - `sonner` — Toast 通知(替代传统 snackbar) diff --git a/README.md b/README.md index 8ec821e..339b113 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,6 @@ - **文件转换**: 支持文件与 Base64 字符串互转. - **图像预览**: 支持图片 Base64 编码与实时预览. -### 📝 Markdown ↔ HTML 转换 - -- **Markdown 转 HTML**: 实时预览 Markdown 渲染结果,支持导出 HTML 源码. -- **HTML 转 Markdown**: 将 HTML 内容转换为 Markdown 格式. - ## 技术栈 - **框架**: [WXT (Web Extension Toolkit)](https://wxt.dev/) diff --git a/package-lock.json b/package-lock.json index 68d015a..82265b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "clsx": "^2.1.1", "dayjs": "^1.11.20", "lucide-react": "^1.16.0", - "marked": "^18.0.4", "qr-scanner": "^1.4.2", "qrious": "^4.0.2", "react": "^19.2.6", @@ -8388,18 +8387,6 @@ "url": "https://github.com/sponsors/fregante" } }, - "node_modules/marked": { - "version": "18.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.4.tgz", - "integrity": "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/marky": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", diff --git a/package.json b/package.json index 576d168..07dd898 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "clsx": "^2.1.1", "dayjs": "^1.11.20", "lucide-react": "^1.16.0", - "marked": "^18.0.4", "qr-scanner": "^1.4.2", "qrious": "^4.0.2", "react": "^19.2.6", diff --git a/public/_locales/zh/messages.json b/public/_locales/zh/messages.json index 9063a45..48d5eb7 100644 --- a/public/_locales/zh/messages.json +++ b/public/_locales/zh/messages.json @@ -131,22 +131,6 @@ "message": "文本、文件与图像的 Base64 编码转换", "description": "Translation key: base64Converter_description" }, - "markdownToHtml_title": { - "message": "Markdown 转 HTML", - "description": "Translation key: markdownToHtml_title" - }, - "markdownToHtml_description": { - "message": "实时 Markdown 转换与 HTML 预览", - "description": "Translation key: markdownToHtml_description" - }, - "htmlToMarkdown_title": { - "message": "HTML 转 Markdown", - "description": "Translation key: htmlToMarkdown_title" - }, - "htmlToMarkdown_description": { - "message": "实时 HTML 转换与 Markdown 预览", - "description": "Translation key: htmlToMarkdown_description" - }, "rightClickRestorer_title": { "message": "右键恢复", "description": "Translation key: rightClickRestorer_title" @@ -291,54 +275,6 @@ "message": "解码大小", "description": "Translation key: base64Converter_decodedSize" }, - "htmlToMarkdown_pageTitle": { - "message": "HTML 转 Markdown", - "description": "Translation key: htmlToMarkdown_pageTitle" - }, - "htmlToMarkdown_splitMode": { - "message": "分屏", - "description": "Translation key: htmlToMarkdown_splitMode" - }, - "htmlToMarkdown_previewMode": { - "message": "预览", - "description": "Translation key: htmlToMarkdown_previewMode" - }, - "htmlToMarkdown_markdownMode": { - "message": "Markdown", - "description": "Translation key: htmlToMarkdown_markdownMode" - }, - "htmlToMarkdown_clear": { - "message": "清空", - "description": "Translation key: htmlToMarkdown_clear" - }, - "htmlToMarkdown_inputLabel": { - "message": "HTML 输入", - "description": "Translation key: htmlToMarkdown_inputLabel" - }, - "htmlToMarkdown_previewLabel": { - "message": "Markdown 预览", - "description": "Translation key: htmlToMarkdown_previewLabel" - }, - "htmlToMarkdown_markdownOutputLabel": { - "message": "Markdown 输出", - "description": "Translation key: htmlToMarkdown_markdownOutputLabel" - }, - "htmlToMarkdown_inputPlaceholder": { - "message": "在此输入 HTML 内容...", - "description": "Translation key: htmlToMarkdown_inputPlaceholder" - }, - "htmlToMarkdown_charCount": { - "message": "{{count}} 字符", - "description": "Translation key: htmlToMarkdown_charCount" - }, - "htmlToMarkdown_download": { - "message": "下载", - "description": "Translation key: htmlToMarkdown_download" - }, - "htmlToMarkdown_emptyHint": { - "message": "输入 HTML 内容以查看转换结果", - "description": "Translation key: htmlToMarkdown_emptyHint" - }, "jsonDiff_pageTitle": { "message": "JSON 差异比较", "description": "Translation key: jsonDiff_pageTitle" @@ -543,54 +479,6 @@ "message": "解析 Payload 失败:", "description": "Translation key: jwt_errors_parsePayloadFailed" }, - "markdownToHtml_pageTitle": { - "message": "Markdown 转 HTML", - "description": "Translation key: markdownToHtml_pageTitle" - }, - "markdownToHtml_splitMode": { - "message": "分屏", - "description": "Translation key: markdownToHtml_splitMode" - }, - "markdownToHtml_previewMode": { - "message": "预览", - "description": "Translation key: markdownToHtml_previewMode" - }, - "markdownToHtml_htmlMode": { - "message": "HTML", - "description": "Translation key: markdownToHtml_htmlMode" - }, - "markdownToHtml_clear": { - "message": "清空", - "description": "Translation key: markdownToHtml_clear" - }, - "markdownToHtml_print": { - "message": "打印", - "description": "Translation key: markdownToHtml_print" - }, - "markdownToHtml_download": { - "message": "下载", - "description": "Translation key: markdownToHtml_download" - }, - "markdownToHtml_inputLabel": { - "message": "Markdown 输入", - "description": "Translation key: markdownToHtml_inputLabel" - }, - "markdownToHtml_previewLabel": { - "message": "实时预览", - "description": "Translation key: markdownToHtml_previewLabel" - }, - "markdownToHtml_htmlOutputLabel": { - "message": "HTML 输出", - "description": "Translation key: markdownToHtml_htmlOutputLabel" - }, - "markdownToHtml_inputPlaceholder": { - "message": "在此输入 Markdown 内容...", - "description": "Translation key: markdownToHtml_inputPlaceholder" - }, - "markdownToHtml_charCount": { - "message": "{{count}} 字符", - "description": "Translation key: markdownToHtml_charCount" - }, "qrCode_pageTitle": { "message": "二维码工具", "description": "Translation key: qrCode_pageTitle" diff --git a/src/config/__tests__/features.test.ts b/src/config/__tests__/features.test.ts index cd273dd..ea1feda 100644 --- a/src/config/__tests__/features.test.ts +++ b/src/config/__tests__/features.test.ts @@ -9,8 +9,8 @@ import { describe('features', () => { describe('FEATURES', () => { - it('should have 11 features defined', () => { - expect(FEATURES).toHaveLength(11); + it('should have 9 features defined', () => { + expect(FEATURES).toHaveLength(9); }); it('should have all required properties for each feature', () => { @@ -95,7 +95,7 @@ describe('features', () => { describe('getAllFeatureKeys', () => { it('should return all feature keys', () => { const allKeys = getAllFeatureKeys(); - expect(allKeys).toHaveLength(11); + expect(allKeys).toHaveLength(9); expect(allKeys).toContain('dashboard'); expect(allKeys).toContain('timestamp'); expect(allKeys).toContain('storageCleaner'); @@ -104,8 +104,6 @@ describe('features', () => { expect(allKeys).toContain('jwt'); expect(allKeys).toContain('jsonDiff'); expect(allKeys).toContain('base64Converter'); - expect(allKeys).toContain('markdownToHtml'); - expect(allKeys).toContain('htmlToMarkdown'); expect(allKeys).toContain('rightClickRestorer'); }); }); @@ -123,9 +121,9 @@ describe('features', () => { expect(pageOrder).toContain('qrCode'); }); - it('should have 10 items in page order', () => { + it('should have 8 items in page order', () => { const pageOrder = getDefaultPageOrder(); - expect(pageOrder).toHaveLength(10); + expect(pageOrder).toHaveLength(8); }); }); }); diff --git a/src/config/features.tsx b/src/config/features.tsx index ed0df11..c069717 100644 --- a/src/config/features.tsx +++ b/src/config/features.tsx @@ -9,8 +9,6 @@ import { Key, GitCompareArrows, ArrowLeftRight, - Code, - File, MousePointerClick, } from 'lucide-react'; @@ -25,8 +23,6 @@ const TextStatisticsPage = lazy(() => import('@/pages/TextStatistics')); const JwtPage = lazy(() => import('@/pages/Jwt')); const JsonToolsPage = lazy(() => import('@/pages/JsonTools')); const Base64ConverterPage = lazy(() => import('@/pages/Base64Converter')); -const MarkdownToHtmlPage = lazy(() => import('@/pages/MarkdownToHtml')); -const HtmlToMarkdownPage = lazy(() => import('@/pages/HtmlToMarkdown')); const RightClickRestorerPage = lazy(() => import('@/pages/RightClickRestorer')); export interface FeatureConfig { @@ -146,32 +142,6 @@ export const FEATURES: FeatureConfig[] = [ tab: Base64ConverterPage, }, }, - { - key: 'markdownToHtml', - labelKey: 'markdownToHtml_title', - descriptionKey: 'markdownToHtml_description', - themeColorKey: 'secondary', - icon: Code, - defaultVisible: true, - components: { - popup: MarkdownToHtmlPage, - sidepanel: MarkdownToHtmlPage, - tab: MarkdownToHtmlPage, - }, - }, - { - key: 'htmlToMarkdown', - labelKey: 'htmlToMarkdown_title', - descriptionKey: 'htmlToMarkdown_description', - themeColorKey: 'secondary', - icon: File, - defaultVisible: true, - components: { - popup: HtmlToMarkdownPage, - sidepanel: HtmlToMarkdownPage, - tab: HtmlToMarkdownPage, - }, - }, { key: 'rightClickRestorer', labelKey: 'rightClickRestorer_title', diff --git a/src/pages/HtmlToMarkdown/index.tsx b/src/pages/HtmlToMarkdown/index.tsx deleted file mode 100644 index bf62a08..0000000 --- a/src/pages/HtmlToMarkdown/index.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useCallback, useMemo, useState } from 'react'; -import { Download, Trash2 } from 'lucide-react'; -import { useI18n } from '@/utils/chromeI18n'; -import CopyButton from '@/components/CopyButton'; -import SwitchButtonGroup from '@/components/SwitchButtonGroup'; -import { Button } from '@/components/ui/button'; -import { useStorageState } from '@/utils/useStorageState'; -import type { HtmlToMarkdownPreviewMode } from '@/types/storage'; -import { downloadMarkdownFile, htmlToMarkdown, SAMPLE_HTML } from '@/utils/htmlToMarkdown'; -import { cn } from '@/lib/utils'; - -const isValidPreviewMode = (val: unknown): val is HtmlToMarkdownPreviewMode => - typeof val === 'string' && ['split', 'preview', 'markdown'].includes(val); - -export default function HtmlToMarkdownPage() { - const { t } = useI18n('htmlToMarkdown'); - const [previewMode, setPreviewMode] = useStorageState( - 'htmlToMarkdown/previewMode', - 'split' as HtmlToMarkdownPreviewMode, - isValidPreviewMode, - ); - const [html, setHtml] = useState(SAMPLE_HTML); - - const result = useMemo(() => htmlToMarkdown(html), [html]); - const error = result.hasError ? (result.error ?? null) : null; - - const handleModeChange = useCallback( - (newMode: HtmlToMarkdownPreviewMode) => { - setPreviewMode(newMode); - }, - [setPreviewMode], - ); - - const handleClear = useCallback(() => { - setHtml(''); - }, []); - - const handleDownload = useCallback(() => { - if (result.markdown) { - downloadMarkdownFile(result.markdown, 'converted.md'); - } - }, [result.markdown]); - - const showInput = previewMode !== 'preview'; - const showOutput = previewMode !== 'markdown'; - - return ( - /* 💡 统一间距尺寸: - - 彻底清除多余的 container max-w-7xl 这种网页大边距, - - 统一收拢为我们先前在 Dashboard 页、JSON 工具箱制定的 p-4 space-y-4 标准极客桌面规格。 - */ -
This is a paragraph.
'); - expect(result.markdown).toContain('This is a paragraph.'); - }); - - it('converts strong and b tags', () => { - const result = htmlToMarkdown('bold and also bold'); - expect(result.markdown).toContain('**bold**'); - expect(result.markdown).toContain('**also bold**'); - }); - - it('converts em and i tags', () => { - const result = htmlToMarkdown('italic and also italic'); - expect(result.markdown).toContain('*italic*'); - expect(result.markdown).toContain('*also italic*'); - }); - - it('converts del and s tags', () => { - const result = htmlToMarkdown('const x = 1;');
- expect(result.markdown).toContain('`const x = 1;`');
- });
-
- it('converts pre code block', () => {
- const result = htmlToMarkdown('line1\nline2');
- expect(result.markdown).toContain('```');
- expect(result.markdown).toContain('line1');
- expect(result.markdown).toContain('line2');
- });
-
- it('converts pre code block with language', () => {
- const result = htmlToMarkdown(
- 'const x = 1;',
- );
- expect(result.markdown).toContain('```javascript');
- expect(result.markdown).toContain('const x = 1;');
- });
-
- it('converts anchor links', () => {
- const result = htmlToMarkdown('Link text');
- expect(result.markdown).toContain('[Link text](https://example.com)');
- });
-
- it('converts anchor links with title', () => {
- const result = htmlToMarkdown('Link');
- expect(result.markdown).toContain('[Link](https://example.com "Title")');
- });
-
- it('converts images', () => {
- const result = htmlToMarkdown('
');
- expect(result.markdown).toContain('');
- });
-
- it('converts images with title', () => {
- const result = htmlToMarkdown('
');
- expect(result.markdown).toContain('');
- });
-
- it('converts unordered list', () => {
- const result = htmlToMarkdown(''); - expect(result.markdown).toContain('> Quote text'); - }); - - it('converts horizontal rule', () => { - const result = htmlToMarkdown('Quote text
| Name | Type |
|---|---|
| John | User |
text
'); - expect(result.markdown).not.toContain('alert'); - expect(result.markdown).not.toContain('.x{}'); - expect(result.markdown).toContain('text'); - }); - - it('handles nested elements', () => { - const result = htmlToMarkdown('bold and italic
'); - expect(result.markdown).toContain('**bold**'); - expect(result.markdown).toContain('*italic*'); - }); - - it('returns hasError false for valid HTML', () => { - const result = htmlToMarkdown('Valid
'); - expect(result.hasError).toBe(false); - expect(result.error).toBeUndefined(); - }); - - it('returns correct length stats', () => { - const html = 'Hello
'; - const result = htmlToMarkdown(html); - expect(result.originalLength).toBe(html.length); - expect(result.markdownLength).toBe(result.markdown.length); - }); - - it('handles full HTML document', () => { - const result = htmlToMarkdown(SAMPLE_HTML); - expect(result.hasError).toBe(false); - expect(result.markdown).toContain('# 欢迎使用 HTML 转 Markdown'); - expect(result.markdown).toContain('**HTML**'); - expect(result.markdown).toContain('*Markdown*'); - expect(result.markdown).toContain('```javascript'); - expect(result.markdown).toContain('| 名称 | 类型 |'); - expect(result.markdown).toContain('> 这是一段引用文本'); - }); - - it('handles plain text without tags', () => { - const result = htmlToMarkdown('Just plain text'); - expect(result.markdown).toContain('Just plain text'); - expect(result.hasError).toBe(false); - }); - - it('handles task list items', () => { - const result = htmlToMarkdown( - 'Content
');
- expect(result.html).toContain(' {
- const result = markdownToHtml('use `npm install` command');
- expect(result.hasError).toBe(false);
- expect(result.html).toContain('npm install');
- });
-
- it('应该转换引用块', () => {
- const result = markdownToHtml('> This is a quote');
- expect(result.hasError).toBe(false);
- expect(result.html).toContain('');
- expect(result.html).toContain('This is a quote');
- });
-
- it('应该转换表格', () => {
- const md = '| A | B |\n|---|---|\n| 1 | 2 |';
- const result = markdownToHtml(md);
- expect(result.hasError).toBe(false);
- expect(result.html).toContain('');
- expect(result.html).toContain('A ');
- expect(result.html).toContain('1 ');
- });
-
- it('应该转换任务列表', () => {
- const result = markdownToHtml('- [x] done\n- [ ] todo');
- expect(result.hasError).toBe(false);
- expect(result.html).toContain(' {
- const result = markdownToHtml('~~deleted~~');
- expect(result.hasError).toBe(false);
- expect(result.html).toContain('deleted');
- });
-
- it('应该处理空字符串', () => {
- const result = markdownToHtml('');
- expect(result.hasError).toBe(false);
- expect(result.html).toBe('');
- expect(result.originalLength).toBe(0);
- expect(result.htmlLength).toBe(0);
- });
-
- it('应该处理空白字符串', () => {
- const result = markdownToHtml(' \n ');
- expect(result.hasError).toBe(false);
- expect(result.html).toBe('');
- });
-
- it('应该处理中文内容', () => {
- const result = markdownToHtml('# 你好世界\n\n这是**中文**内容。');
- expect(result.hasError).toBe(false);
- expect(result.html).toContain('你好世界');
- expect(result.html).toContain('中文');
- });
-
- it('应该转换示例 Markdown', () => {
- const result = markdownToHtml(SAMPLE_MARKDOWN);
- expect(result.hasError).toBe(false);
- expect(result.html).toContain('');
- expect(result.html).toContain('');
- expect(result.originalLength).toBe(SAMPLE_MARKDOWN.length);
- expect(result.htmlLength).toBeGreaterThan(result.originalLength);
- });
-});
-
-describe('wrapHtmlDocument', () => {
- it('应该生成完整的 HTML 文档', () => {
- const doc = wrapHtmlDocument('Hello
', 'Test Title');
- expect(doc).toContain('');
- expect(doc).toContain('');
- expect(doc).toContain('Test Title ');
- expect(doc).toContain('');
- expect(doc).toContain('Hello
');
- expect(doc).toContain('');
- });
-
- it('应该转义标题中的特殊字符', () => {
- const doc = wrapHtmlDocument('test
', 'Title