From 5a4b3e66bde0a0396937983cd1b38d399f81d315 Mon Sep 17 00:00:00 2001
From: taiyi
Date: Thu, 16 Oct 2025 22:15:22 +0800
Subject: [PATCH] =?UTF-8?q?1.=20=E6=94=AF=E6=8C=81markdown=E5=92=8C?=
=?UTF-8?q?=E5=AF=8C=E6=96=87=E6=9C=AC=E7=BC=96=E8=BE=91=E5=99=A8=202.=20?=
=?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BB=A3=E7=A0=81=E5=9D=97=E5=92=8C=E5=9B=BE?=
=?UTF-8?q?=E7=89=87=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 166 ++++++++++++++++++++++++++++--
main.js | 276 ++++++++++++++++++++++++++++++++++++++------------
manifest.json | 4 +-
3 files changed, 368 insertions(+), 78 deletions(-)
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)
+
+![[Obsidian 图片语法.png]]
+```
+
+### 分割线
+```markdown
+---
+***
+___
+```
+
+## 🎨 转换示例
+
+### 输入(Markdown)
+```markdown
+## 测试文档
+
+这是一段**粗体**文本和*斜体*文本。
+
+- 列表项 1
+- 列表项 2
+
+> 这是引用内容
+
+代码示例:`console.log("Hello")`
+
+
+```
+
+### 输出(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 = ``;
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}>`);
+ }
+ 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}>`);
+ }
+ listType = 'ol';
+ inList = true;
+ listItems = [];
+ }
+ listItems.push(`${olMatch[1]}`);
+ } else {
+ // 非列表行
+ if (inList && listItems.length > 0) {
+ processedListLines.push(`<${listType}>${listItems.join('')}${listType}>`);
+ listItems = [];
+ inList = false;
+ listType = null;
+ }
+ processedListLines.push(line);
+ }
+ }
+ // 处理末尾的列表
+ if (inList && listItems.length > 0) {
+ processedListLines.push(`<${listType}>${listItems.join('')}${listType}>`);
+ }
- // 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 = `
+