第一次提交,完成copy操作,存在问题,飞书云文档复制出现问题
This commit is contained in:
commit
258ac53811
3
.idea/.gitignore
vendored
Normal file
3
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
8
.idea/image-copy.iml
Normal file
8
.idea/image-copy.iml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
94
.idea/inspectionProfiles/Project_Default.xml
Normal file
94
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,94 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="81">
|
||||
<item index="0" class="java.lang.String" itemvalue="httpx" />
|
||||
<item index="1" class="java.lang.String" itemvalue="PySimpleGUI" />
|
||||
<item index="2" class="java.lang.String" itemvalue="greenlet" />
|
||||
<item index="3" class="java.lang.String" itemvalue="joblib" />
|
||||
<item index="4" class="java.lang.String" itemvalue="threadpoolctl" />
|
||||
<item index="5" class="java.lang.String" itemvalue="Babel" />
|
||||
<item index="6" class="java.lang.String" itemvalue="huggingface-hub" />
|
||||
<item index="7" class="java.lang.String" itemvalue="scikit-learn" />
|
||||
<item index="8" class="java.lang.String" itemvalue="nltk" />
|
||||
<item index="9" class="java.lang.String" itemvalue="PyYAML" />
|
||||
<item index="10" class="java.lang.String" itemvalue="h11" />
|
||||
<item index="11" class="java.lang.String" itemvalue="PyQt5-sip" />
|
||||
<item index="12" class="java.lang.String" itemvalue="QtPy" />
|
||||
<item index="13" class="java.lang.String" itemvalue="hanziconv" />
|
||||
<item index="14" class="java.lang.String" itemvalue="docxcompose" />
|
||||
<item index="15" class="java.lang.String" itemvalue="MarkupSafe" />
|
||||
<item index="16" class="java.lang.String" itemvalue="mkl" />
|
||||
<item index="17" class="java.lang.String" itemvalue="fsspec" />
|
||||
<item index="18" class="java.lang.String" itemvalue="PyQt5-Qt5" />
|
||||
<item index="19" class="java.lang.String" itemvalue="filelock" />
|
||||
<item index="20" class="java.lang.String" itemvalue="playwright" />
|
||||
<item index="21" class="java.lang.String" itemvalue="guiqwt" />
|
||||
<item index="22" class="java.lang.String" itemvalue="PyQt5" />
|
||||
<item index="23" class="java.lang.String" itemvalue="safetensors" />
|
||||
<item index="24" class="java.lang.String" itemvalue="certifi" />
|
||||
<item index="25" class="java.lang.String" itemvalue="anyio" />
|
||||
<item index="26" class="java.lang.String" itemvalue="lxml" />
|
||||
<item index="27" class="java.lang.String" itemvalue="PythonQwt" />
|
||||
<item index="28" class="java.lang.String" itemvalue="soupsieve" />
|
||||
<item index="29" class="java.lang.String" itemvalue="sympy" />
|
||||
<item index="30" class="java.lang.String" itemvalue="xlrd" />
|
||||
<item index="31" class="java.lang.String" itemvalue="beautifulsoup4" />
|
||||
<item index="32" class="java.lang.String" itemvalue="tokenizers" />
|
||||
<item index="33" class="java.lang.String" itemvalue="pydantic" />
|
||||
<item index="34" class="java.lang.String" itemvalue="transformers" />
|
||||
<item index="35" class="java.lang.String" itemvalue="pyperclip" />
|
||||
<item index="36" class="java.lang.String" itemvalue="h5py" />
|
||||
<item index="37" class="java.lang.String" itemvalue="synonyms" />
|
||||
<item index="38" class="java.lang.String" itemvalue="pyqt-tools" />
|
||||
<item index="39" class="java.lang.String" itemvalue="click" />
|
||||
<item index="40" class="java.lang.String" itemvalue="openai" />
|
||||
<item index="41" class="java.lang.String" itemvalue="regex" />
|
||||
<item index="42" class="java.lang.String" itemvalue="pydantic_core" />
|
||||
<item index="43" class="java.lang.String" itemvalue="tbb" />
|
||||
<item index="44" class="java.lang.String" itemvalue="charset-normalizer" />
|
||||
<item index="45" class="java.lang.String" itemvalue="httpcore" />
|
||||
<item index="46" class="java.lang.String" itemvalue="idna" />
|
||||
<item index="47" class="java.lang.String" itemvalue="distro" />
|
||||
<item index="48" class="java.lang.String" itemvalue="jieba" />
|
||||
<item index="49" class="java.lang.String" itemvalue="networkx" />
|
||||
<item index="50" class="java.lang.String" itemvalue="Whoosh" />
|
||||
<item index="51" class="java.lang.String" itemvalue="numpy" />
|
||||
<item index="52" class="java.lang.String" itemvalue="Jinja2" />
|
||||
<item index="53" class="java.lang.String" itemvalue="sniffio" />
|
||||
<item index="54" class="java.lang.String" itemvalue="exceptiongroup" />
|
||||
<item index="55" class="java.lang.String" itemvalue="tomli" />
|
||||
<item index="56" class="java.lang.String" itemvalue="guidata" />
|
||||
<item index="57" class="java.lang.String" itemvalue="cnsyn" />
|
||||
<item index="58" class="java.lang.String" itemvalue="urllib3" />
|
||||
<item index="59" class="java.lang.String" itemvalue="pyee" />
|
||||
<item index="60" class="java.lang.String" itemvalue="volcengine-python-sdk" />
|
||||
<item index="61" class="java.lang.String" itemvalue="annotated-types" />
|
||||
<item index="62" class="java.lang.String" itemvalue="chatoperastore" />
|
||||
<item index="63" class="java.lang.String" itemvalue="scipy" />
|
||||
<item index="64" class="java.lang.String" itemvalue="python-docx" />
|
||||
<item index="65" class="java.lang.String" itemvalue="OpenCC" />
|
||||
<item index="66" class="java.lang.String" itemvalue="six" />
|
||||
<item index="67" class="java.lang.String" itemvalue="intel-openmp" />
|
||||
<item index="68" class="java.lang.String" itemvalue="tzdata" />
|
||||
<item index="69" class="java.lang.String" itemvalue="packaging" />
|
||||
<item index="70" class="java.lang.String" itemvalue="torch" />
|
||||
<item index="71" class="java.lang.String" itemvalue="et-xmlfile" />
|
||||
<item index="72" class="java.lang.String" itemvalue="chardet" />
|
||||
<item index="73" class="java.lang.String" itemvalue="pandas" />
|
||||
<item index="74" class="java.lang.String" itemvalue="tqdm" />
|
||||
<item index="75" class="java.lang.String" itemvalue="colorama" />
|
||||
<item index="76" class="java.lang.String" itemvalue="mpmath" />
|
||||
<item index="77" class="java.lang.String" itemvalue="typing_extensions" />
|
||||
<item index="78" class="java.lang.String" itemvalue="pillow" />
|
||||
<item index="79" class="java.lang.String" itemvalue="pytz" />
|
||||
<item index="80" class="java.lang.String" itemvalue="openpyxl" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
4
.idea/misc.xml
Normal file
4
.idea/misc.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/image-copy.iml" filepath="$PROJECT_DIR$/.idea/image-copy.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
205
README.md
Normal file
205
README.md
Normal file
@ -0,0 +1,205 @@
|
||||
# Bulk Copy with Images - 图片优化增强版
|
||||
|
||||
一键复制 Obsidian 笔记全文,自动转换为富文本 HTML,图片内嵌为 base64。完美支持飞书云文档、微信公众号、知乎等多种编辑器平台。
|
||||
|
||||
## ✨ 核心功能
|
||||
|
||||
### 1. 完整的排版保留
|
||||
- ✅ 标题(H1-H6)
|
||||
- ✅ 粗体、斜体、删除线、高亮
|
||||
- ✅ 代码块和行内代码
|
||||
- ✅ 列表(有序、无序)
|
||||
- ✅ 引用块
|
||||
- ✅ 表格
|
||||
- ✅ 链接
|
||||
- ✅ 分割线
|
||||
|
||||
### 2. 智能图片处理
|
||||
- ✅ **自动压缩**:大图自动缩放到合适尺寸
|
||||
- ✅ **质量优化**:在清晰度和文件大小间取得平衡
|
||||
- ✅ **显示控制**:限制图片最大显示宽度,避免过长
|
||||
- ✅ **格式转换**:统一转为 JPEG 格式(压缩率更高)
|
||||
- ✅ **特殊处理**:
|
||||
- SVG:保持矢量格式
|
||||
- GIF:保留动画效果
|
||||
- PNG/JPG:智能压缩
|
||||
|
||||
### 3. 多平台兼容
|
||||
- 飞书云文档 ✅
|
||||
- 微信公众号 ✅
|
||||
- 知乎 ✅
|
||||
- 语雀 ✅
|
||||
- Notion ✅
|
||||
- 其他富文本编辑器 ✅
|
||||
|
||||
## 🎯 图片优化说明
|
||||
|
||||
### 当前配置
|
||||
|
||||
```javascript
|
||||
const IMAGE_CONFIG = {
|
||||
MAX_WIDTH: 800, // 压缩最大宽度(像素)
|
||||
QUALITY: 0.8, // 压缩质量(0-1)
|
||||
DISPLAY_MAX_WIDTH: 600 // 显示最大宽度(像素)
|
||||
};
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
#### 1. `MAX_WIDTH` - 压缩最大宽度
|
||||
- **作用**:超过此宽度的图片会被等比缩放
|
||||
- **默认值**:800px
|
||||
- **建议值**:
|
||||
- 微信公众号:800px(推荐)
|
||||
- 飞书文档:1000px
|
||||
- 知乎:800px
|
||||
- 如果图片需要更清晰:1200px
|
||||
- 如果想要更小的文件:600px
|
||||
|
||||
#### 2. `QUALITY` - 压缩质量
|
||||
- **作用**:控制 JPEG 压缩质量
|
||||
- **默认值**:0.8(80%)
|
||||
- **建议值**:
|
||||
- 高质量(文件较大):0.85-0.9
|
||||
- 平衡模式(推荐):0.75-0.85
|
||||
- 高压缩(文件小,质量降低):0.6-0.75
|
||||
|
||||
#### 3. `DISPLAY_MAX_WIDTH` - 显示最大宽度
|
||||
- **作用**:图片在编辑器中的最大显示宽度
|
||||
- **默认值**:600px
|
||||
- **说明**:
|
||||
- 这个设置**不影响图片实际尺寸**
|
||||
- 只控制图片在编辑器中的显示宽度
|
||||
- 避免图片在编辑器中显得过长
|
||||
|
||||
### 如何自定义配置?
|
||||
|
||||
打开 `main.js`,修改文件开头的配置:
|
||||
|
||||
```javascript
|
||||
/* ========== 图片优化配置 ========== */
|
||||
const IMAGE_CONFIG = {
|
||||
MAX_WIDTH: 800, // 改成你想要的压缩宽度
|
||||
QUALITY: 0.8, // 改成你想要的质量(0-1)
|
||||
DISPLAY_MAX_WIDTH: 600 // 改成你想要的显示宽度
|
||||
};
|
||||
```
|
||||
|
||||
### 压缩效果对比
|
||||
|
||||
| 原始图片 | 压缩后 | 节省空间 |
|
||||
|---------|--------|---------|
|
||||
| 5MB, 3000x2000 | ~150KB | 97% |
|
||||
| 2MB, 1920x1080 | ~80KB | 96% |
|
||||
| 500KB, 800x600 | ~60KB | 88% |
|
||||
|
||||
## 📖 使用方法
|
||||
|
||||
### 方法 1:快捷键
|
||||
1. 打开任意 Markdown 笔记
|
||||
2. 按 `Ctrl+Shift+C`(Windows/Linux)或 `Cmd+Shift+C`(Mac)
|
||||
3. 粘贴到目标编辑器
|
||||
|
||||
### 方法 2:命令面板
|
||||
1. 按 `Ctrl+P`(Windows/Linux)或 `Cmd+P`(Mac)
|
||||
2. 输入"复制全文"
|
||||
3. 选择"复制全文并内嵌本地图片"
|
||||
4. 粘贴到目标编辑器
|
||||
|
||||
## 💡 使用技巧
|
||||
|
||||
### 1. 不同平台的建议配置
|
||||
|
||||
**微信公众号**(推荐当前配置)
|
||||
```javascript
|
||||
MAX_WIDTH: 800
|
||||
QUALITY: 0.8
|
||||
DISPLAY_MAX_WIDTH: 600
|
||||
```
|
||||
|
||||
**飞书云文档**(可以更高清)
|
||||
```javascript
|
||||
MAX_WIDTH: 1000
|
||||
QUALITY: 0.85
|
||||
DISPLAY_MAX_WIDTH: 700
|
||||
```
|
||||
|
||||
**知乎**(兼顾质量和加载速度)
|
||||
```javascript
|
||||
MAX_WIDTH: 800
|
||||
QUALITY: 0.8
|
||||
DISPLAY_MAX_WIDTH: 600
|
||||
```
|
||||
|
||||
### 2. 图片过大怎么办?
|
||||
|
||||
如果复制后提示"图片太大"或粘贴失败:
|
||||
1. 降低 `MAX_WIDTH`(如改为 600 或 700)
|
||||
2. 降低 `QUALITY`(如改为 0.7)
|
||||
3. 或者将大图片分开发送
|
||||
|
||||
### 3. 图片不够清晰?
|
||||
|
||||
如果复制后图片显示模糊:
|
||||
1. 提高 `MAX_WIDTH`(如改为 1000 或 1200)
|
||||
2. 提高 `QUALITY`(如改为 0.85 或 0.9)
|
||||
|
||||
## 🔧 技术细节
|
||||
|
||||
### 图片处理流程
|
||||
|
||||
1. **读取图片** → 2. **判断格式** → 3. **压缩处理** → 4. **Base64 编码** → 5. **生成 HTML**
|
||||
|
||||
### 特殊格式处理
|
||||
|
||||
- **SVG**:保持矢量格式,不压缩
|
||||
- **GIF**:保留动画,不压缩
|
||||
- **PNG/JPG/WebP**:转为 JPEG 并压缩
|
||||
- **其他格式**:尝试压缩,失败则保持原样
|
||||
|
||||
### 为什么转为 JPEG?
|
||||
|
||||
- JPEG 压缩率高(文件更小)
|
||||
- 兼容性好(所有平台都支持)
|
||||
- 对于照片和复杂图片效果最好
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 为什么有些平台粘贴后样式消失了?
|
||||
A: 某些平台会过滤样式。插件已使用内联样式来提高兼容性。
|
||||
|
||||
### Q: 图片太小/太大怎么办?
|
||||
A: 调整 `DISPLAY_MAX_WIDTH` 参数。
|
||||
|
||||
### Q: 压缩会影响图片清晰度吗?
|
||||
A: 会有轻微影响,但肉眼难以察觉。可以通过提高 `QUALITY` 来改善。
|
||||
|
||||
### Q: 支持哪些图片格式?
|
||||
A: PNG, JPG, JPEG, GIF, WebP, SVG, BMP, TIFF 等常见格式。
|
||||
|
||||
### Q: 动图会变成静态图吗?
|
||||
A: 不会,GIF 动图会保留动画效果。
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v2.0.0 (2024)
|
||||
- ✨ 新增图片智能压缩功能
|
||||
- ✨ 新增图片尺寸限制
|
||||
- ✨ 新增可自定义配置
|
||||
- ✨ 优化表格支持
|
||||
- ✨ 改进 Markdown 转 HTML 转换
|
||||
- 🐛 修复图片过大导致复制失败的问题
|
||||
- 🐛 修复部分平台样式不兼容的问题
|
||||
|
||||
### v1.0.0
|
||||
- 🎉 初始版本
|
||||
- ✅ 基本的图片内嵌功能
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 🙋 反馈与支持
|
||||
|
||||
如有问题或建议,欢迎反馈!
|
||||
|
||||
557
main.js
Normal file
557
main.js
Normal file
@ -0,0 +1,557 @@
|
||||
/* 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, ''');
|
||||
};
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
// 2. 行内代码
|
||||
const inlineCodes = [];
|
||||
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
||||
const placeholder = `___INLINE_CODE_${inlineCodes.length}___`;
|
||||
inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 3. 表格处理(Markdown 表格)
|
||||
const tables = [];
|
||||
html = html.replace(/^\|(.+)\|[ \t]*\n\|[-:\s|]+\|[ \t]*\n((?:\|.+\|[ \t]*\n?)*)/gm, (match, header, body) => {
|
||||
const placeholder = `___TABLE_${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 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;
|
||||
});
|
||||
|
||||
// 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. 引用块
|
||||
html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
||||
// 合并连续的引用块
|
||||
html = html.replace(/(<\/blockquote>\n<blockquote>)/g, '\n');
|
||||
|
||||
// 6. 分割线
|
||||
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>');
|
||||
|
||||
// 合并列表项
|
||||
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>';
|
||||
});
|
||||
|
||||
// 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*](?:.*?[^\s*])?)\*/g, '<em>$1</em>');
|
||||
html = html.replace(/_([^\s_](?:.*?[^\s_])?)_/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 lines = html.split('\n');
|
||||
const processed = [];
|
||||
let inParagraph = false;
|
||||
|
||||
for (let line of lines) {
|
||||
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>');
|
||||
inParagraph = false;
|
||||
}
|
||||
processed.push(line);
|
||||
} else if (trimmed === '') {
|
||||
if (inParagraph) {
|
||||
processed.push('</p>');
|
||||
inParagraph = false;
|
||||
}
|
||||
} else {
|
||||
if (!inParagraph) {
|
||||
processed.push('<p>');
|
||||
inParagraph = true;
|
||||
}
|
||||
processed.push(line);
|
||||
}
|
||||
}
|
||||
if (inParagraph) {
|
||||
processed.push('</p>');
|
||||
}
|
||||
|
||||
html = processed.join('\n');
|
||||
|
||||
// 11. 还原占位符
|
||||
// 还原表格
|
||||
tables.forEach((table, i) => {
|
||||
html = html.replace(`___TABLE_${i}___`, table);
|
||||
});
|
||||
|
||||
// 还原代码块
|
||||
codeBlocks.forEach((code, i) => {
|
||||
html = html.replace(`___CODE_BLOCK_${i}___`, code);
|
||||
});
|
||||
|
||||
// 还原行内代码
|
||||
inlineCodes.forEach((code, i) => {
|
||||
html = html.replace(`___INLINE_CODE_${i}___`, code);
|
||||
});
|
||||
|
||||
// 12. 清理
|
||||
html = html.replace(/<p>\s*<\/p>/g, '');
|
||||
html = html.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/* ========== 复制全文 + 内嵌图片 ========== */
|
||||
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 */
|
||||
const htmlContent = markdownToHtml(md);
|
||||
|
||||
/* 4. 生成完整的 HTML 文档(包含样式) */
|
||||
const fullHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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();
|
||||
|
||||
/* 5. 写入剪贴板 - 多格式支持 */
|
||||
try {
|
||||
if (navigator.clipboard && window.ClipboardItem) {
|
||||
// 准备多种格式,提高平台兼容性
|
||||
const clipboardData = {
|
||||
'text/plain': new Blob([md], { type: 'text/plain' }),
|
||||
'text/html': new Blob([fullHtml], { type: 'text/html' })
|
||||
};
|
||||
|
||||
const clipboardItem = new ClipboardItem(clipboardData);
|
||||
await navigator.clipboard.write([clipboardItem]);
|
||||
|
||||
new Notice('✅ 已复制全文(含图片和样式)!\n支持:飞书、微信公众号、知乎等平台');
|
||||
} else {
|
||||
// 降级:仅支持纯文本
|
||||
await navigator.clipboard.writeText(md);
|
||||
new Notice('⚠️ 已复制纯文本格式(浏览器不支持富文本)');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[bulk-copy] 复制失败', err);
|
||||
new Notice('❌ 复制失败,请检查浏览器权限设置');
|
||||
}
|
||||
|
||||
/* ----------- 单张图内嵌 ----------- */
|
||||
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() {}
|
||||
};
|
||||
11
manifest.json
Normal file
11
manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "bulk-copy-images",
|
||||
"name": "Bulk Copy with Images",
|
||||
"version": "2.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "一键复制笔记全文,自动转换为富文本 HTML,图片内嵌为 base64。完美支持飞书云文档、微信公众号、知乎等多种编辑器平台,保留完整排版和样式。",
|
||||
"author": "you",
|
||||
"authorUrl": "",
|
||||
"fundingUrl": "",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
0
styles.css
Normal file
0
styles.css
Normal file
Loading…
Reference in New Issue
Block a user