701 lines
22 KiB
JavaScript
701 lines
22 KiB
JavaScript
/* eslint-disable */
|
||
const { Plugin, Notice, TFile } = require('obsidian');
|
||
|
||
/* ========== 图片优化配置 ========== */
|
||
const IMAGE_CONFIG = {
|
||
// 图片压缩最大宽度(像素)- 超过此宽度的图片会被等比缩放
|
||
MAX_WIDTH: 800,
|
||
|
||
// 图片压缩质量(0-1 之间,1 为最高质量)
|
||
// 推荐值:0.8-0.85,在质量和文件大小之间取得良好平衡
|
||
QUALITY: 0.8,
|
||
|
||
// 显示最大宽度(像素)- 图片在编辑器中的最大显示宽度
|
||
DISPLAY_MAX_WIDTH: 600
|
||
};
|
||
|
||
/* ========== 安全大图转 base64 ========== */
|
||
async function buf2base64(buf) {
|
||
// 使用更直接的方式转换为 base64,避免数据损失
|
||
return new Promise((resolve, reject) => {
|
||
const bytes = new Uint8Array(buf);
|
||
let binary = '';
|
||
for (let i = 0; i < bytes.byteLength; i++) {
|
||
binary += String.fromCharCode(bytes[i]);
|
||
}
|
||
resolve(btoa(binary));
|
||
});
|
||
}
|
||
|
||
/* ========== 图片压缩和尺寸限制 ========== */
|
||
async function compressImage(buf, ext, maxWidth = IMAGE_CONFIG.MAX_WIDTH, quality = IMAGE_CONFIG.QUALITY) {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// 创建 Blob
|
||
const mime = getMime(ext);
|
||
const blob = new Blob([buf], { type: mime });
|
||
|
||
// 创建图片元素
|
||
const img = new Image();
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
img.onload = () => {
|
||
// 释放临时 URL
|
||
URL.revokeObjectURL(url);
|
||
|
||
// 计算新尺寸(保持宽高比)
|
||
let width = img.width;
|
||
let height = img.height;
|
||
|
||
if (width > maxWidth) {
|
||
height = Math.round((height * maxWidth) / width);
|
||
width = maxWidth;
|
||
}
|
||
|
||
// 如果图片本来就很小,且不需要转换格式,直接返回
|
||
if (width === img.width && (ext.toLowerCase() === 'jpg' || ext.toLowerCase() === 'jpeg' || ext.toLowerCase() === 'png')) {
|
||
resolve({ data: buf, needsConversion: false });
|
||
return;
|
||
}
|
||
|
||
// 创建 Canvas 进行压缩
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0, width, height);
|
||
|
||
// 转换为 Blob(JPEG 格式压缩效果更好)
|
||
canvas.toBlob(
|
||
(compressedBlob) => {
|
||
if (!compressedBlob) {
|
||
reject(new Error('图片压缩失败'));
|
||
return;
|
||
}
|
||
|
||
// 将 Blob 转为 ArrayBuffer
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
resolve({
|
||
data: reader.result,
|
||
needsConversion: true,
|
||
mime: 'image/jpeg' // 统一转为 JPEG
|
||
});
|
||
};
|
||
reader.onerror = reject;
|
||
reader.readAsArrayBuffer(compressedBlob);
|
||
},
|
||
'image/jpeg', // 使用 JPEG 格式
|
||
quality // 压缩质量
|
||
);
|
||
};
|
||
|
||
img.onerror = () => {
|
||
URL.revokeObjectURL(url);
|
||
reject(new Error('图片加载失败'));
|
||
};
|
||
|
||
img.src = url;
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ========== mime 映射 ========== */
|
||
const getMime = ext => ({
|
||
png: 'image/png',
|
||
jpg: 'image/jpeg',
|
||
jpeg: 'image/jpeg',
|
||
gif: 'image/gif',
|
||
svg: 'image/svg+xml',
|
||
webp: 'image/webp',
|
||
bmp: 'image/bmp',
|
||
ico: 'image/x-icon',
|
||
tiff: 'image/tiff',
|
||
tif: 'image/tiff'
|
||
}[ext.toLowerCase()] || 'application/octet-stream');
|
||
|
||
/* ========== Markdown 转 HTML(保留完整排版) ========== */
|
||
function markdownToHtml(md) {
|
||
let html = md;
|
||
|
||
// 转义 HTML 特殊字符(仅用于代码块)
|
||
const escapeHtml = (text) => {
|
||
return text
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.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(/```[\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(/`([^`\n]+)`/g, (match, code) => {
|
||
const id = inlineCodes.length;
|
||
inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
|
||
return `<!--INLINECODE${id}-->`;
|
||
});
|
||
|
||
// 3. 表格处理(Markdown 表格)
|
||
const tables = [];
|
||
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 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 `\n<!--TABLE${id}-->\n`;
|
||
});
|
||
|
||
// 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>');
|
||
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
|
||
html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
|
||
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
|
||
|
||
// 5. 引用块
|
||
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 />');
|
||
|
||
// 7. 列表处理(改进版,支持嵌套)
|
||
const listLines = html.split('\n');
|
||
const processedListLines = [];
|
||
let inList = false;
|
||
let listType = null; // 'ul' or 'ol'
|
||
let listItems = [];
|
||
|
||
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}>`);
|
||
}
|
||
|
||
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>');
|
||
|
||
// 粗体(**text** 或 __text__)
|
||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
||
|
||
// 斜体(*text* 或 _text_)
|
||
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>');
|
||
|
||
// 高亮(==text==)
|
||
html = html.replace(/==(.+?)==/g, '<mark>$1</mark>');
|
||
|
||
// 9. 链接 [text](url)
|
||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||
|
||
// 10. 段落处理(改进版)
|
||
const finalLines = html.split('\n');
|
||
const finalProcessed = [];
|
||
let inParagraph = false;
|
||
let paragraphLines = [];
|
||
|
||
for (let i = 0; i < finalLines.length; i++) {
|
||
const line = finalLines[i];
|
||
const trimmed = line.trim();
|
||
|
||
// 检查是否是块级元素或占位符
|
||
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;
|
||
}
|
||
if (trimmed !== '') {
|
||
finalProcessed.push(line);
|
||
}
|
||
} else {
|
||
// 普通文本行
|
||
if (!inParagraph) {
|
||
inParagraph = true;
|
||
}
|
||
paragraphLines.push(line);
|
||
}
|
||
}
|
||
|
||
// 处理末尾的段落
|
||
if (inParagraph && paragraphLines.length > 0) {
|
||
finalProcessed.push('<p>' + paragraphLines.join('\n') + '</p>');
|
||
}
|
||
|
||
html = finalProcessed.join('\n');
|
||
|
||
// 11. 换行处理(Markdown 中的双空格 + 换行)
|
||
html = html.replace(/ \n/g, '<br>\n');
|
||
|
||
// 12. 还原占位符(按顺序还原,使用正则全局替换)
|
||
// 先还原块级元素(表格、代码块)
|
||
tables.forEach((table, i) => {
|
||
html = html.replace(new RegExp(`<!--TABLE${i}-->`, 'g'), table);
|
||
});
|
||
|
||
codeBlocks.forEach((code, i) => {
|
||
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(new RegExp(`<!--INLINECODE${i}-->`, 'g'), code);
|
||
});
|
||
|
||
// 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');
|
||
|
||
// 调试:检查是否还有未还原的占位符
|
||
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();
|
||
}
|
||
|
||
/* ========== 复制全文 + 内嵌图片 ========== */
|
||
async function copyActiveFileWithImages(plugin) {
|
||
const file = plugin.app.workspace.getActiveFile();
|
||
if (!file || file.extension !== 'md') {
|
||
new Notice('请先打开一篇 Markdown 笔记');
|
||
return;
|
||
}
|
||
|
||
let md = await plugin.app.vault.read(file);
|
||
const baseFolder = file.parent.path;
|
||
const assetsFolder = `${baseFolder}/${file.basename}.assets`;
|
||
|
||
/* 1. Wiki 语法 ![[xxx.png]] */
|
||
md = await replaceAsync(md, /!\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g,
|
||
async (_, raw) => {
|
||
const name = raw.split('|')[0].trim();
|
||
return await inlineImage(name, '');
|
||
});
|
||
|
||
/* 2. 标准语法  */
|
||
md = await replaceAsync(md, /!\[([^\]]*)\]\(([^)]+)\)/g,
|
||
async (_, alt, src) => {
|
||
src = decodeURIComponent(src).trim();
|
||
return await inlineImage(src, alt);
|
||
});
|
||
|
||
/* 3. 转换为 HTML */
|
||
let htmlContent = markdownToHtml(md);
|
||
|
||
// 调试:输出转换后的 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 {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* 标题样式 */
|
||
h1, h2, h3, h4, h5, h6 {
|
||
margin-top: 24px;
|
||
margin-bottom: 16px;
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
}
|
||
h1 { font-size: 2em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
|
||
h2 { font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
|
||
h3 { font-size: 1.25em; }
|
||
|
||
/* 段落 */
|
||
p {
|
||
margin-top: 0;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 图片样式 - 关键优化 */
|
||
img {
|
||
max-width: ${IMAGE_CONFIG.DISPLAY_MAX_WIDTH}px;
|
||
width: 100%;
|
||
height: auto;
|
||
display: block;
|
||
margin: 15px auto;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
/* 代码块 */
|
||
pre {
|
||
background-color: #f6f8fa;
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
overflow: auto;
|
||
margin: 16px 0;
|
||
}
|
||
code {
|
||
background-color: rgba(175, 184, 193, 0.2);
|
||
padding: 0.2em 0.4em;
|
||
border-radius: 6px;
|
||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||
font-size: 85%;
|
||
}
|
||
pre code {
|
||
background-color: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
/* 引用块 */
|
||
blockquote {
|
||
padding: 0 1em;
|
||
color: #6a737d;
|
||
border-left: 0.25em solid #dfe2e5;
|
||
margin: 16px 0;
|
||
}
|
||
|
||
/* 列表 */
|
||
ul, ol {
|
||
padding-left: 2em;
|
||
margin: 16px 0;
|
||
}
|
||
li {
|
||
margin: 0.25em 0;
|
||
}
|
||
|
||
/* 分割线 */
|
||
hr {
|
||
height: 0.25em;
|
||
padding: 0;
|
||
margin: 24px 0;
|
||
background-color: #e1e4e8;
|
||
border: 0;
|
||
}
|
||
|
||
/* 链接 */
|
||
a {
|
||
color: #0366d6;
|
||
text-decoration: none;
|
||
}
|
||
a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* 删除线 */
|
||
del {
|
||
text-decoration: line-through;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* 高亮 */
|
||
mark {
|
||
background-color: #fff3cd;
|
||
padding: 0.2em 0;
|
||
}
|
||
|
||
/* 表格(如果有) */
|
||
table {
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
margin: 16px 0;
|
||
}
|
||
th, td {
|
||
border: 1px solid #dfe2e5;
|
||
padding: 6px 13px;
|
||
}
|
||
th {
|
||
background-color: #f6f8fa;
|
||
font-weight: 600;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
${htmlContent}
|
||
</body>
|
||
</html>
|
||
`.trim();
|
||
|
||
/* 6. 写入剪贴板 - 多格式优化 */
|
||
try {
|
||
if (navigator.clipboard && window.ClipboardItem) {
|
||
// 准备多种格式,提高平台兼容性
|
||
// 使用 styledHtml(简化版)为主,fullHtml 为备用
|
||
const clipboardData = {
|
||
'text/plain': new Blob([md], { type: 'text/plain' }),
|
||
'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 {
|
||
// 降级方案:尝试使用传统方法
|
||
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('❌ 复制失败: ' + err.message);
|
||
}
|
||
|
||
/* ----------- 单张图内嵌 ----------- */
|
||
async function inlineImage(imgName, alt) {
|
||
const candidates = [
|
||
`${assetsFolder}/${imgName}`,
|
||
`${baseFolder}/${imgName}`,
|
||
imgName.startsWith('/') ? imgName.slice(1) : imgName
|
||
];
|
||
let imgFile = candidates
|
||
.map(p => plugin.app.vault.getAbstractFileByPath(p))
|
||
.find(f => f instanceof TFile);
|
||
if (!imgFile) { // 全库搜文件名
|
||
const all = plugin.app.vault.getFiles();
|
||
imgFile = all.find(f => f.name === imgName);
|
||
}
|
||
if (!imgFile) {
|
||
console.warn('[bulk-copy] 未找到图片文件', imgName);
|
||
return ``;
|
||
}
|
||
|
||
try {
|
||
const buf = await plugin.app.vault.readBinary(imgFile);
|
||
const ext = imgFile.extension;
|
||
const mime = getMime(ext);
|
||
|
||
// 特殊处理 SVG 文件(不压缩)
|
||
if (ext.toLowerCase() === 'svg') {
|
||
// 对于 SVG,直接读取文本内容
|
||
const svgContent = await plugin.app.vault.read(imgFile);
|
||
const base64Svg = btoa(unescape(encodeURIComponent(svgContent)));
|
||
// 限制 SVG 最大宽度
|
||
return `<img src="data:${mime};base64,${base64Svg}" alt="${alt}" style="max-width: ${IMAGE_CONFIG.DISPLAY_MAX_WIDTH}px; width: 100%; height: auto; display: block; margin: 15px auto;" />`;
|
||
}
|
||
|
||
// GIF 动图不压缩,保持动画效果
|
||
if (ext.toLowerCase() === 'gif') {
|
||
const data = await buf2base64(buf);
|
||
const altText = alt ? alt.replace(/"/g, '"') : '';
|
||
const imgStyle = `max-width: ${IMAGE_CONFIG.DISPLAY_MAX_WIDTH}px; width: 100%; height: auto; display: block; margin: 15px auto;`;
|
||
return `<img src="data:${mime};base64,${data}" alt="${altText}" style="${imgStyle}" />`;
|
||
}
|
||
|
||
// 处理其他图片格式:压缩图片
|
||
const compressed = await compressImage(buf, ext, IMAGE_CONFIG.MAX_WIDTH, IMAGE_CONFIG.QUALITY);
|
||
|
||
let finalMime = mime;
|
||
let finalData;
|
||
|
||
if (compressed.needsConversion) {
|
||
// 图片已被压缩,使用压缩后的数据
|
||
finalMime = compressed.mime;
|
||
finalData = await buf2base64(compressed.data);
|
||
} else {
|
||
// 图片无需压缩,使用原始数据
|
||
finalData = await buf2base64(compressed.data);
|
||
}
|
||
|
||
if (!finalData) {
|
||
throw new Error('Base64 转换失败');
|
||
}
|
||
|
||
// 添加内联样式,限制图片显示宽度
|
||
// 某些平台(如微信公众号)会过滤外部样式,所以使用内联样式
|
||
const imgStyle = `max-width: ${IMAGE_CONFIG.DISPLAY_MAX_WIDTH}px; width: 100%; height: auto; display: block; margin: 15px auto; border-radius: 4px;`;
|
||
const altText = alt ? alt.replace(/"/g, '"') : '';
|
||
|
||
return `<img src="data:${finalMime};base64,${finalData}" alt="${altText}" style="${imgStyle}" />`;
|
||
} catch (e) {
|
||
console.warn('[bulk-copy] 读取失败', imgName, e);
|
||
return ``;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ========== 异步 replace ========== */
|
||
async function replaceAsync(str, regex, asyncFn) {
|
||
const promises = [];
|
||
str.replace(regex, (match, ...args) => {
|
||
promises.push(asyncFn(match, ...args));
|
||
return match;
|
||
});
|
||
const reps = await Promise.all(promises);
|
||
return str.replace(regex, () => reps.shift());
|
||
}
|
||
|
||
/* ========== 插件入口 ========== */
|
||
module.exports = class BulkCopyImagesPlugin extends Plugin {
|
||
onload() {
|
||
this.addCommand({
|
||
id: 'bulk-copy-with-images',
|
||
name: '复制全文并内嵌本地图片',
|
||
hotkeys: [{ modifiers: ['Ctrl', 'Shift'], key: 'c' }],
|
||
callback: () => copyActiveFileWithImages(this)
|
||
});
|
||
}
|
||
onunload() {}
|
||
}; |