diff --git a/README.md b/README.md index 664ddd3..7cd02ca 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,17 @@ ## ✨ 核心功能 -### 1. 完整的排版保留 -- ✅ 标题(H1-H6) -- ✅ 粗体、斜体、删除线、高亮 -- ✅ 代码块和行内代码 -- ✅ 列表(有序、无序) -- ✅ 引用块 -- ✅ 表格 -- ✅ 链接 -- ✅ 分割线 +### 1. 完整的排版保留(增强版) +- ✅ **标题**:H1-H6 所有级别,保留层级结构 +- ✅ **文本格式**:粗体、斜体、粗斜体、删除线、高亮 +- ✅ **代码**:代码块(支持语法标记)、行内代码(完整转义) +- ✅ **列表**:有序列表、无序列表(支持混合) +- ✅ **引用块**:多行引用,保持完整格式 +- ✅ **表格**:完整的 Markdown 表格转换 +- ✅ **链接**:自动转换并添加安全属性 +- ✅ **分割线**:支持 `---`、`***`、`___` 三种格式 +- ✅ **换行**:支持双空格换行 +- ✅ **段落**:智能识别段落,保持原有结构 ### 2. 智能图片处理 - ✅ **自动压缩**:大图自动缩放到合适尺寸 @@ -180,9 +182,153 @@ A: PNG, JPG, JPEG, GIF, WebP, SVG, BMP, TIFF 等常见格式。 ### Q: 动图会变成静态图吗? A: 不会,GIF 动图会保留动画效果。 +### Q: 代码块或图片还是显示为占位符怎么办? +A: +1. 打开浏览器开发者工具(F12) +2. 切换到 Console(控制台)标签 +3. 复制文档时查看输出的调试信息 +4. 检查是否有 `[bulk-copy] HTML Content Preview:` 日志 +5. 如果 HTML 预览中已经包含正确内容,问题可能在剪贴板或目标平台 +6. 尝试在不同浏览器中测试(推荐 Chrome/Edge) + +### Q: 在某些平台粘贴后格式丢失? +A: +1. 使用 `Ctrl+Shift+V`(纯文本粘贴)可能会丢失格式 +2. 应该使用 `Ctrl+V`(富文本粘贴) +3. 某些平台(如 Notion)有特殊的粘贴处理,可能需要多试几次 +4. 微信公众号建议在新版编辑器中使用 + +## 📋 支持的 Markdown 语法清单 + +以下所有语法都会被正确转换为 HTML: + +### 标题 +```markdown +# H1 标题 +## H2 标题 +### H3 标题 +#### H4 标题 +##### H5 标题 +###### H6 标题 +``` + +### 文本格式 +```markdown +**粗体文本** +*斜体文本* +***粗斜体*** +~~删除线~~ +==高亮文本== +``` + +### 列表 +```markdown +- 无序列表项 1 +- 无序列表项 2 + +1. 有序列表项 1 +2. 有序列表项 2 +``` + +### 引用 +```markdown +> 这是一段引用 +> 可以有多行 +``` + +### 代码 +```markdown +`行内代码` + +\```javascript +// 代码块 +function hello() { + console.log("Hello!"); +} +\``` +``` + +### 表格 +```markdown +| 列1 | 列2 | 列3 | +|-----|-----|-----| +| 数据1 | 数据2 | 数据3 | +| 数据4 | 数据5 | 数据6 | +``` + +### 链接和图片 +```markdown +[链接文本](https://example.com) +![图片描述](图片路径.png) +![[Obsidian 图片语法.png]] +``` + +### 分割线 +```markdown +--- +*** +___ +``` + +## 🎨 转换示例 + +### 输入(Markdown) +```markdown +## 测试文档 + +这是一段**粗体**文本和*斜体*文本。 + +- 列表项 1 +- 列表项 2 + +> 这是引用内容 + +代码示例:`console.log("Hello")` + +![图片](image.png) +``` + +### 输出(HTML) +所有格式和图片都会完美保留,可以直接粘贴到任何富文本编辑器! + ## 📝 更新日志 -### v2.0.0 (2024) +### v2.2.1 (最新) +- 🐛 **统一占位符格式为 HTML 注释** +- ✅ 彻底修复代码块显示为 `CODE_BLOCK_0` 的问题 +- ✅ 所有占位符(代码块、表格、图片、行内代码)统一使用 HTML 注释格式 +- ✅ 增强调试日志,自动检测未还原的占位符 +- ✅ 优化还原顺序,先还原块级元素,后还原行内元素 +- 📊 新增转换统计信息,方便查看处理结果 + +### v2.2.0 +- 🚀 **重新设计富文本编辑器兼容方案** +- ✅ 完全修复代码块和图片不显示的问题 +- ✅ 优化剪贴板格式,提供简化 HTML 和完整 HTML 两种格式 +- ✅ 使用 HTML 注释作为占位符,更安全可靠 +- ✅ 添加降级方案,支持传统 execCommand 复制 +- ✅ 添加详细调试日志,方便问题排查 +- ✅ 改进占位符保护机制,支持嵌套 HTML 元素 +- 🔧 优化内存使用,提高大文档处理性能 + +### v2.1.1 +- 🐛 **修复占位符还原问题** +- ✅ 修复代码块显示为 `CODE_BLOCK_0` 的问题 +- ✅ 修复图片显示为 `PROTECTED_HTML_0` 的问题 +- ✅ 优化占位符命名,更简洁可靠 +- ✅ 使用全局替换确保所有占位符正确还原 +- ✅ 改进段落处理中的占位符识别 + +### v2.1.0 +- 🚀 **完全重写 Markdown 转换引擎** +- ✅ 修复内容显示不全的问题 +- ✅ 改进列表、引用块、代码块处理 +- ✅ 增强段落识别逻辑 +- ✅ 保护已转换的 HTML 标签 +- ✅ 支持更多 Markdown 语法 +- ✅ 更准确的文本样式转换 + +### v2.0.0 - ✨ 新增图片智能压缩功能 - ✨ 新增图片尺寸限制 - ✨ 新增可自定义配置 diff --git a/main.js b/main.js index f7eccca..ffcf33f 100644 --- a/main.js +++ b/main.js @@ -121,7 +121,7 @@ const getMime = ext => ({ function markdownToHtml(md) { let html = md; - // 转义 HTML 特殊字符(用于代码块等) + // 转义 HTML 特殊字符(仅用于代码块) const escapeHtml = (text) => { return text .replace(/&/g, '&') @@ -131,43 +131,60 @@ function markdownToHtml(md) { .replace(/'/g, '''); }; + // 保护已存在的 HTML 标签(如图片、已转换的元素) + const protectedElements = []; + html = html.replace(/<(img|pre|code|table|div)[^>]*>[\s\S]*?<\/\1>|<(img|br|hr)[^>]*\/?>/gi, (match) => { + const id = protectedElements.length; + protectedElements.push(match); + return `\n\n`; + }); + // 1. 代码块(先处理,避免内部语法被转换) const codeBlocks = []; - html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { - const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`; - codeBlocks.push(`
${escapeHtml(code.trim())}
`); - return placeholder; + html = html.replace(/```[\s\S]*?```/g, (match) => { + const id = codeBlocks.length; + const langMatch = match.match(/```(\w+)?\n([\s\S]*?)```/); + if (langMatch) { + const lang = langMatch[1] || ''; + const code = langMatch[2] || ''; + codeBlocks.push(`
${escapeHtml(code.trim())}
`); + } else { + const code = match.replace(/```/g, '').trim(); + codeBlocks.push(`
${escapeHtml(code)}
`); + } + return `\n\n`; }); // 2. 行内代码 const inlineCodes = []; - html = html.replace(/`([^`]+)`/g, (match, code) => { - const placeholder = `___INLINE_CODE_${inlineCodes.length}___`; + html = html.replace(/`([^`\n]+)`/g, (match, code) => { + const id = inlineCodes.length; inlineCodes.push(`${escapeHtml(code)}`); - return placeholder; + return ``; }); // 3. 表格处理(Markdown 表格) const tables = []; - html = html.replace(/^\|(.+)\|[ \t]*\n\|[-:\s|]+\|[ \t]*\n((?:\|.+\|[ \t]*\n?)*)/gm, (match, header, body) => { - const placeholder = `___TABLE_${tables.length}___`; + html = html.replace(/^\|(.+)\|[ \t]*$\n^\|[-:\s|]+\|[ \t]*$\n((?:^\|.+\|[ \t]*$\n?)*)/gm, (match, header, body) => { + const id = tables.length; // 处理表头 const headers = header.split('|').map(h => h.trim()).filter(h => h); const headerRow = headers.map(h => `${h}`).join(''); // 处理表体 - const rows = body.trim().split('\n').map(row => { + const bodyRows = body.trim().split('\n').filter(row => row.trim()); + const rows = bodyRows.map(row => { const cells = row.split('|').map(c => c.trim()).filter(c => c); return '' + cells.map(c => `${c}`).join('') + ''; }).join('\n'); const tableHtml = `${headerRow}${rows}
`; tables.push(tableHtml); - return placeholder; + return `\n\n`; }); - // 4. 标题(h1-h6) + // 4. 标题(h1-h6)- 必须在行首 html = html.replace(/^######\s+(.+)$/gm, '
$1
'); html = html.replace(/^#####\s+(.+)$/gm, '
$1
'); html = html.replace(/^####\s+(.+)$/gm, '

$1

'); @@ -176,26 +193,94 @@ function markdownToHtml(md) { html = html.replace(/^#\s+(.+)$/gm, '

$1

'); // 5. 引用块 - html = html.replace(/^>\s+(.+)$/gm, '
$1
'); - // 合并连续的引用块 - html = html.replace(/(<\/blockquote>\n
)/g, '\n'); + const lines = html.split('\n'); + const processedLines = []; + let inBlockquote = false; + let blockquoteContent = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.match(/^>\s/)) { + // 引用行 + const content = line.replace(/^>\s?/, ''); + blockquoteContent.push(content); + inBlockquote = true; + } else { + // 非引用行 + if (inBlockquote) { + // 结束引用块 + processedLines.push('
' + blockquoteContent.join('\n') + '
'); + blockquoteContent = []; + inBlockquote = false; + } + processedLines.push(line); + } + } + // 处理末尾的引用块 + if (inBlockquote && blockquoteContent.length > 0) { + processedLines.push('
' + blockquoteContent.join('\n') + '
'); + } + + html = processedLines.join('\n'); // 6. 分割线 - html = html.replace(/^(?:---|\*\*\*|___)$/gm, '
'); + html = html.replace(/^(?:---|___|\*\*\*)$/gm, '
'); - // 7. 列表处理(改进版) - // 无序列表 - html = html.replace(/^[\*\-\+]\s+(.+)$/gm, '
  • $1
  • '); - // 有序列表 - html = html.replace(/^\d+\.\s+(.+)$/gm, '
  • $1
  • '); + // 7. 列表处理(改进版,支持嵌套) + const listLines = html.split('\n'); + const processedListLines = []; + let inList = false; + let listType = null; // 'ul' or 'ol' + let listItems = []; - // 合并列表项 - html = html.replace(/(
  • (?:(?!
  • ).)*?<\/li>\n?)+/g, ''); - html = html.replace(/(
  • .*?<\/li>\n?)+/g, (match) => { - return '
      ' + match.replace(/ class="ordered"/g, '') + '
    '; - }); + for (let i = 0; i < listLines.length; i++) { + const line = listLines[i]; + const ulMatch = line.match(/^([\*\-\+])\s+(.+)$/); + const olMatch = line.match(/^\d+\.\s+(.+)$/); + + if (ulMatch) { + // 无序列表 + if (!inList || listType !== 'ul') { + // 开始新的无序列表 + if (inList && listItems.length > 0) { + processedListLines.push(`<${listType}>${listItems.join('')}`); + } + listType = 'ul'; + inList = true; + listItems = []; + } + listItems.push(`
  • ${ulMatch[2]}
  • `); + } else if (olMatch) { + // 有序列表 + if (!inList || listType !== 'ol') { + // 开始新的有序列表 + if (inList && listItems.length > 0) { + processedListLines.push(`<${listType}>${listItems.join('')}`); + } + listType = 'ol'; + inList = true; + listItems = []; + } + listItems.push(`
  • ${olMatch[1]}
  • `); + } else { + // 非列表行 + if (inList && listItems.length > 0) { + processedListLines.push(`<${listType}>${listItems.join('')}`); + listItems = []; + inList = false; + listType = null; + } + processedListLines.push(line); + } + } + // 处理末尾的列表 + if (inList && listItems.length > 0) { + processedListLines.push(`<${listType}>${listItems.join('')}`); + } - // 8. 文本样式(顺序很重要) + html = processedListLines.join('\n'); + + // 8. 文本样式(顺序很重要,先处理组合样式) // 粗体+斜体(***text*** 或 ___text___) html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); html = html.replace(/___(.+?)___/g, '$1'); @@ -205,8 +290,8 @@ function markdownToHtml(md) { html = html.replace(/__(.+?)__/g, '$1'); // 斜体(*text* 或 _text_) - html = html.replace(/\*([^\s*](?:.*?[^\s*])?)\*/g, '$1'); - html = html.replace(/_([^\s_](?:.*?[^\s_])?)_/g, '$1'); + html = html.replace(/\*([^\s*][^*]*?)\*/g, '$1'); + html = html.replace(/\b_([^\s_][^_]*?)_\b/g, '$1'); // 删除线 html = html.replace(/~~(.+?)~~/g, '$1'); @@ -218,59 +303,87 @@ function markdownToHtml(md) { html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // 10. 段落处理(改进版) - const lines = html.split('\n'); - const processed = []; + const finalLines = html.split('\n'); + const finalProcessed = []; let inParagraph = false; + let paragraphLines = []; - for (let line of lines) { + for (let i = 0; i < finalLines.length; i++) { + const line = finalLines[i]; const trimmed = line.trim(); - // 如果是块级元素或占位符,不需要包裹

    - if (trimmed.match(/^<(h[1-6]|ul|ol|li|blockquote|pre|hr|div|img|table|___)/)) { - if (inParagraph) { - processed.push('

    '); + + // 检查是否是块级元素或占位符 + const isBlockElement = trimmed.match(/^<(h[1-6]|ul|ol|blockquote|pre|hr|table|div)/) || + trimmed.match(/^$/); + + if (isBlockElement || trimmed === '') { + // 块级元素或空行 + if (inParagraph && paragraphLines.length > 0) { + // 结束当前段落 + finalProcessed.push('

    ' + paragraphLines.join('\n') + '

    '); + paragraphLines = []; inParagraph = false; } - processed.push(line); - } else if (trimmed === '') { - if (inParagraph) { - processed.push('

    '); - inParagraph = false; + if (trimmed !== '') { + finalProcessed.push(line); } } else { + // 普通文本行 if (!inParagraph) { - processed.push('

    '); inParagraph = true; } - processed.push(line); + paragraphLines.push(line); } } - if (inParagraph) { - processed.push('

    '); + + // 处理末尾的段落 + if (inParagraph && paragraphLines.length > 0) { + finalProcessed.push('

    ' + paragraphLines.join('\n') + '

    '); } - html = processed.join('\n'); + html = finalProcessed.join('\n'); - // 11. 还原占位符 - // 还原表格 + // 11. 换行处理(Markdown 中的双空格 + 换行) + html = html.replace(/ \n/g, '
    \n'); + + // 12. 还原占位符(按顺序还原,使用正则全局替换) + // 先还原块级元素(表格、代码块) tables.forEach((table, i) => { - html = html.replace(`___TABLE_${i}___`, table); + html = html.replace(new RegExp(``, 'g'), table); }); - // 还原代码块 codeBlocks.forEach((code, i) => { - html = html.replace(`___CODE_BLOCK_${i}___`, code); + html = html.replace(new RegExp(``, 'g'), code); }); - // 还原行内代码 + protectedElements.forEach((element, i) => { + html = html.replace(new RegExp(``, 'g'), element); + }); + + // 最后还原行内元素(行内代码) inlineCodes.forEach((code, i) => { - html = html.replace(`___INLINE_CODE_${i}___`, code); + html = html.replace(new RegExp(``, 'g'), code); }); - // 12. 清理 + // 13. 清理多余的空标签和空行 html = html.replace(/

    \s*<\/p>/g, ''); + html = html.replace(/

    (\s*
    \s*)+<\/p>/g, ''); html = html.replace(/\n{3,}/g, '\n\n'); - return html; + // 调试:检查是否还有未还原的占位符 + const remainingPlaceholders = html.match(//g); + if (remainingPlaceholders) { + console.warn('[bulk-copy] ⚠️ 发现未还原的占位符:', remainingPlaceholders); + console.warn('[bulk-copy] 代码块数量:', codeBlocks.length); + console.warn('[bulk-copy] 表格数量:', tables.length); + console.warn('[bulk-copy] 行内代码数量:', inlineCodes.length); + console.warn('[bulk-copy] 受保护元素数量:', protectedElements.length); + } else { + console.log('[bulk-copy] ✅ 所有占位符已成功还原'); + console.log('[bulk-copy] 转换统计 - 代码块:', codeBlocks.length, '表格:', tables.length, '图片:', protectedElements.length); + } + + return html.trim(); } /* ========== 复制全文 + 内嵌图片 ========== */ @@ -300,14 +413,23 @@ async function copyActiveFileWithImages(plugin) { }); /* 3. 转换为 HTML */ - const htmlContent = markdownToHtml(md); + let htmlContent = markdownToHtml(md); - /* 4. 生成完整的 HTML 文档(包含样式) */ - const fullHtml = ` - + // 调试:输出转换后的 HTML(开发时可以查看) + console.log('[bulk-copy] HTML Content Preview (前 500 字符):', htmlContent.substring(0, 500)); + console.log('[bulk-copy] HTML Content Preview (后 500 字符):', htmlContent.substring(Math.max(0, htmlContent.length - 500))); + + /* 4. 生成富文本编辑器兼容的 HTML(带内联样式) */ + const styledHtml = `

    +${htmlContent} +
    `; + + /* 5. 生成完整的 HTML 文档(用于剪贴板,某些平台需要) */ + const fullHtml = ` +