1. 支持markdown和富文本编辑器

2. 完善代码块和图片复制粘贴
This commit is contained in:
taiyi 2025-10-16 22:15:22 +08:00
parent 258ac53811
commit 5a4b3e66bd
3 changed files with 368 additions and 78 deletions

166
README.md
View File

@ -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
- ✨ 新增图片智能压缩功能
- ✨ 新增图片尺寸限制
- ✨ 新增可自定义配置

276
main.js
View File

@ -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<!--PROTECTED${id}-->\n`;
});
// 1. 代码块(先处理,避免内部语法被转换)
const codeBlocks = [];
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`;
codeBlocks.push(`<pre><code class="language-${lang || ''}">${escapeHtml(code.trim())}</code></pre>`);
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(`<pre><code class="language-${lang}">${escapeHtml(code.trim())}</code></pre>`);
} else {
const code = match.replace(/```/g, '').trim();
codeBlocks.push(`<pre><code>${escapeHtml(code)}</code></pre>`);
}
return `\n<!--CODEBLOCK${id}-->\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(`<code>${escapeHtml(code)}</code>`);
return placeholder;
return `<!--INLINECODE${id}-->`;
});
// 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 => `<th>${h}</th>`).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 '<tr>' + cells.map(c => `<td>${c}</td>`).join('') + '</tr>';
}).join('\n');
const tableHtml = `<table><thead><tr>${headerRow}</tr></thead><tbody>${rows}</tbody></table>`;
tables.push(tableHtml);
return placeholder;
return `\n<!--TABLE${id}-->\n`;
});
// 4. 标题h1-h6
// 4. 标题h1-h6- 必须在行首
html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
@ -176,26 +193,94 @@ function markdownToHtml(md) {
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
// 5. 引用块
html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
// 合并连续的引用块
html = html.replace(/(<\/blockquote>\n<blockquote>)/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('<blockquote>' + blockquoteContent.join('\n') + '</blockquote>');
blockquoteContent = [];
inBlockquote = false;
}
processedLines.push(line);
}
}
// 处理末尾的引用块
if (inBlockquote && blockquoteContent.length > 0) {
processedLines.push('<blockquote>' + blockquoteContent.join('\n') + '</blockquote>');
}
html = processedLines.join('\n');
// 6. 分割线
html = html.replace(/^(?:---|\*\*\*|___)$/gm, '<hr />');
html = html.replace(/^(?:---|___|\*\*\*)$/gm, '<hr />');
// 7. 列表处理(改进版)
// 无序列表
html = html.replace(/^[\*\-\+]\s+(.+)$/gm, '<li>$1</li>');
// 有序列表
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li class="ordered">$1</li>');
// 7. 列表处理(改进版,支持嵌套)
const listLines = html.split('\n');
const processedListLines = [];
let inList = false;
let listType = null; // 'ul' or 'ol'
let listItems = [];
// 合并列表项
html = html.replace(/(<li>(?:(?!<li class="ordered">).)*?<\/li>\n?)+/g, '<ul>$&</ul>');
html = html.replace(/(<li class="ordered">.*?<\/li>\n?)+/g, (match) => {
return '<ol>' + match.replace(/ class="ordered"/g, '') + '</ol>';
});
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(`<li>${ulMatch[2]}</li>`);
} 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(`<li>${olMatch[1]}</li>`);
} 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, '<strong><em>$1</em></strong>');
html = html.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>');
@ -205,8 +290,8 @@ function markdownToHtml(md) {
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
// 斜体(*text* 或 _text_
html = html.replace(/\*([^\s*](?:.*?[^\s*])?)\*/g, '<em>$1</em>');
html = html.replace(/_([^\s_](?:.*?[^\s_])?)_/g, '<em>$1</em>');
html = html.replace(/\*([^\s*][^*]*?)\*/g, '<em>$1</em>');
html = html.replace(/\b_([^\s_][^_]*?)_\b/g, '<em>$1</em>');
// 删除线
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
@ -218,59 +303,87 @@ function markdownToHtml(md) {
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
// 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();
// 如果是块级元素或占位符,不需要包裹 <p>
if (trimmed.match(/^<(h[1-6]|ul|ol|li|blockquote|pre|hr|div|img|table|___)/)) {
if (inParagraph) {
processed.push('</p>');
// 检查是否是块级元素或占位符
const isBlockElement = trimmed.match(/^<(h[1-6]|ul|ol|blockquote|pre|hr|table|div)/) ||
trimmed.match(/^<!--(?:CODEBLOCK|TABLE|PROTECTED)\d+-->$/);
if (isBlockElement || trimmed === '') {
// 块级元素或空行
if (inParagraph && paragraphLines.length > 0) {
// 结束当前段落
finalProcessed.push('<p>' + paragraphLines.join('\n') + '</p>');
paragraphLines = [];
inParagraph = false;
}
processed.push(line);
} else if (trimmed === '') {
if (inParagraph) {
processed.push('</p>');
inParagraph = false;
if (trimmed !== '') {
finalProcessed.push(line);
}
} else {
// 普通文本行
if (!inParagraph) {
processed.push('<p>');
inParagraph = true;
}
processed.push(line);
paragraphLines.push(line);
}
}
if (inParagraph) {
processed.push('</p>');
// 处理末尾的段落
if (inParagraph && paragraphLines.length > 0) {
finalProcessed.push('<p>' + paragraphLines.join('\n') + '</p>');
}
html = processed.join('\n');
html = finalProcessed.join('\n');
// 11. 还原占位符
// 还原表格
// 11. 换行处理Markdown 中的双空格 + 换行)
html = html.replace(/ \n/g, '<br>\n');
// 12. 还原占位符(按顺序还原,使用正则全局替换)
// 先还原块级元素(表格、代码块)
tables.forEach((table, i) => {
html = html.replace(`___TABLE_${i}___`, table);
html = html.replace(new RegExp(`<!--TABLE${i}-->`, 'g'), table);
});
// 还原代码块
codeBlocks.forEach((code, i) => {
html = html.replace(`___CODE_BLOCK_${i}___`, code);
html = html.replace(new RegExp(`<!--CODEBLOCK${i}-->`, 'g'), code);
});
// 还原行内代码
protectedElements.forEach((element, i) => {
html = html.replace(new RegExp(`<!--PROTECTED${i}-->`, 'g'), element);
});
// 最后还原行内元素(行内代码)
inlineCodes.forEach((code, i) => {
html = html.replace(`___INLINE_CODE_${i}___`, code);
html = html.replace(new RegExp(`<!--INLINECODE${i}-->`, 'g'), code);
});
// 12. 清理
// 13. 清理多余的空标签和空行
html = html.replace(/<p>\s*<\/p>/g, '');
html = html.replace(/<p>(\s*<br>\s*)+<\/p>/g, '');
html = html.replace(/\n{3,}/g, '\n\n');
return html;
// 调试:检查是否还有未还原的占位符
const remainingPlaceholders = html.match(/<!--(?:CODEBLOCK|TABLE|PROTECTED|INLINECODE)\d+-->/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 = `
<!DOCTYPE html>
// 调试:输出转换后的 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 = `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 900px;">
${htmlContent}
</div>`;
/* 5. 生成完整的 HTML 文档(用于剪贴板,某些平台需要) */
const fullHtml = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* 通用样式 - 适配多平台 */
body {
@ -436,27 +558,49 @@ ${htmlContent}
</html>
`.trim();
/* 5. 写入剪贴板 - 多格式支持 */
/* 6. 写入剪贴板 - 多格式优化 */
try {
if (navigator.clipboard && window.ClipboardItem) {
// 准备多种格式,提高平台兼容性
// 使用 styledHtml简化版为主fullHtml 为备用
const clipboardData = {
'text/plain': new Blob([md], { type: 'text/plain' }),
'text/html': new Blob([fullHtml], { type: 'text/html' })
'text/html': new Blob([styledHtml], { type: 'text/html' })
};
const clipboardItem = new ClipboardItem(clipboardData);
await navigator.clipboard.write([clipboardItem]);
console.log('[bulk-copy] 复制成功HTML 长度:', styledHtml.length);
new Notice('✅ 已复制全文(含图片和样式)!\n支持飞书、微信公众号、知乎等平台');
} else {
// 降级:仅支持纯文本
await navigator.clipboard.writeText(md);
new Notice('⚠️ 已复制纯文本格式(浏览器不支持富文本)');
// 降级方案:尝试使用传统方法
const clipboardDiv = document.createElement('div');
clipboardDiv.style.position = 'fixed';
clipboardDiv.style.left = '-9999px';
clipboardDiv.innerHTML = styledHtml;
document.body.appendChild(clipboardDiv);
const range = document.createRange();
range.selectNodeContents(clipboardDiv);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
try {
document.execCommand('copy');
new Notice('✅ 已复制全文(使用传统方式)');
} catch (e) {
await navigator.clipboard.writeText(md);
new Notice('⚠️ 已复制纯文本格式');
}
document.body.removeChild(clipboardDiv);
selection.removeAllRanges();
}
} catch (err) {
console.error('[bulk-copy] 复制失败', err);
new Notice('❌ 复制失败,请检查浏览器权限设置');
new Notice('❌ 复制失败: ' + err.message);
}
/* ----------- 单张图内嵌 ----------- */

View File

@ -1,9 +1,9 @@
{
"id": "bulk-copy-images",
"name": "Bulk Copy with Images",
"version": "2.0.0",
"version": "2.2.1",
"minAppVersion": "0.15.0",
"description": "一键复制笔记全文,自动转换为富文本 HTML图片内嵌为 base64。完美支持飞书云文档、微信公众号、知乎等多种编辑器平台,保留完整排版和样式。",
"description": "一键复制笔记全文,自动转换为富文本 HTML图片智能压缩并内嵌为 base64。完美支持飞书云文档、微信公众号、知乎等富文本编辑器平台。统一的 HTML 注释占位符机制确保代码块、图片、表格完美还原显示。",
"author": "you",
"authorUrl": "",
"fundingUrl": "",