main.js
'use strict';
var obsidian = require('obsidian');
const DEFAULT_SETTINGS = {
removeListBlankLines: true,
cleanBooksCopyright: true,
autoAddListFormat: false,
debugMode: false
};
class PasteOptimizerPlugin extends obsidian.Plugin {
async onload() {
await this.loadSettings();
this.log('粘贴优化插件已加载');
// 统一的粘贴事件处理器
this.registerEvent(
this.app.workspace.on('editor-paste', (evt, editor) => {
const text = evt.clipboardData?.getData('text/plain');
if (!text) return;
let processedText = text;
let shouldIntercept = false;
const processedFeatures = [];
// 功能 1: 清理 Books 版权信息
if (this.settings.cleanBooksCopyright) {
const result = this.cleanBooksCopyright(text);
if (result.processed) {
processedText = result.text;
shouldIntercept = true;
processedFeatures.push('Books版权清理');
this.log('清理前:', text.substring(0, 100));
this.log('清理后:', processedText.substring(0, 100));
}
}
// 功能 2: 去除列表项之间的空行
if (this.settings.removeListBlankLines) {
const result = this.removeListBlankLines(processedText);
if (result.processed) {
processedText = result.text;
shouldIntercept = true;
processedFeatures.push('列表空行去除');
}
}
// 如果有任何处理,则拦截默认粘贴
if (shouldIntercept) {
evt.preventDefault();
editor.replaceSelection(processedText);
// 只在处理了内容时才显示通知
if (processedFeatures.length > 0) {
this.log(`已应用: ${processedFeatures.join('、')}`);
}
}
// 延迟处理:处理从 Obsidian 内部复制的情况
if (this.settings.removeListBlankLines) {
setTimeout(() => {
this.cleanupEditorLists(editor);
}, 50);
}
})
);
// 命令: 手动去除选中区域的列表空行
this.addCommand({
id: 'remove-blank-lines',
name: '去除列表项之间的空行',
editorCallback: (editor) => {
const selection = editor.getSelection();
if (selection) {
const result = this.removeListBlankLines(selection);
if (result.processed) {
editor.replaceSelection(result.text);
new obsidian.Notice('已去除选中区域的列表空行');
} else {
new obsidian.Notice('选中区域没有需要处理的列表空行');
}
} else {
const fullText = editor.getValue();
const result = this.removeListBlankLines(fullText);
if (result.processed) {
editor.setValue(result.text);
new obsidian.Notice('已去除文档中的列表空行');
} else {
new obsidian.Notice('文档中没有需要处理的列表空行');
}
}
}
});
// 命令: 清理当前列表
this.addCommand({
id: 'clean-current-list',
name: '清理当前列表的空行',
editorCallback: (editor) => {
const removed = this.cleanupEditorLists(editor);
if (removed > 0) {
new obsidian.Notice(`已清理 ${removed} 个空行`);
} else {
new obsidian.Notice('未找到需要清理的空行');
}
}
});
// 命令: 清理 Books 版权信息
this.addCommand({
id: 'clean-books-copyright',
name: '清理 Books 版权信息',
editorCallback: (editor) => {
const selection = editor.getSelection();
if (!selection) {
new obsidian.Notice('请先选中要清理的文本');
return;
}
const result = this.cleanBooksCopyright(selection);
if (result.processed) {
editor.replaceSelection(result.text);
new obsidian.Notice('已清理 Books 版权信息');
} else {
new obsidian.Notice('未检测到 Books 版权信息');
}
}
});
// 添加设置页面
this.addSettingTab(new PasteOptimizerSettingTab(this.app, this));
}
/**
* 清理 Books 应用的版权信息
*/
cleanBooksCopyright(text) {
const hasBooksCopyright =
text.includes('摘录来自') ||
text.includes('此材料可能受版权保护') ||
text.includes('Excerpt From') ||
text.includes('This material may be protected by copyright');
if (!hasBooksCopyright) {
return { text, processed: false };
}
// 清理版权信息
let cleaned = text
.replace(/摘录来自[\s\S]*?此材料可能受版权保护。?\s*/g, '')
.replace(/Excerpt From[\s\S]*?This material may be protected by copyright\.?\s*/g, '')
.trim();
// 移除所有类型的引号(开头和结尾)
cleaned = this.removeQuotes(cleaned);
// 处理每一行
const lines = cleaned
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
// 如果启用自动列表格式
if (this.settings.autoAddListFormat) {
cleaned = lines
.map(line => {
// 如果已经是列表项,不重复添加
if (/^[-*+]\s/.test(line)) {
return line;
}
return '- ' + line;
})
.join('\n');
} else {
// 保留原格式
cleaned = lines.join('\n');
}
return { text: cleaned, processed: true };
}
/**
* 移除文本首尾的各种引号
*/
removeQuotes(text) {
const quoteChars = /[\u0022\u0027\u2018\u2019\u201C\u201D\u201E\u201F\u2033\u300C-\u300F\u00AB\u00BB\u2039\u203A《》]/g;
let cleaned = text.trim();
let prev;
let iterations = 0;
// 多次迭代,直到没有引号或达到最大次数
do {
prev = cleaned;
// 移除首尾的引号和空白
cleaned = cleaned.replace(/^[\s\u0022\u0027\u2018\u2019\u201C\u201D\u201E\u201F\u2033\u300C-\u300F\u00AB\u00BB\u2039\u203A《》]+/, '');
cleaned = cleaned.replace(/[\s\u0022\u0027\u2018\u2019\u201C\u201D\u201E\u201F\u2033\u300C-\u300F\u00AB\u00BB\u2039\u203A《》]+$/, '');
cleaned = cleaned.trim();
iterations++;
} while (cleaned !== prev && iterations < 20);
return cleaned;
}
/**
* 去除列表项之间的空行
*/
removeListBlankLines(text) {
const hasListItems = /^[\s]*[-*+]\s/m.test(text);
const hasBlankLines = /\n\s*\n/.test(text);
if (!hasListItems || !hasBlankLines) {
return { text, processed: false };
}
const lines = text.split('\n');
const result = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
const isBlankLine = trimmedLine === '';
// 检查是否应该跳过这个空行
if (isBlankLine && i > 0 && i < lines.length - 1) {
const prevLine = lines[i - 1].trim();
const nextLine = lines[i + 1].trim();
const isPrevList = /^[-*+]\s/.test(prevLine);
const isNextList = /^[-*+]\s/.test(nextLine);
// 如果前后都是列表项,跳过这个空行
if (isPrevList && isNextList) {
this.log(`跳过空行 (第${i+1}行): 前="${prevLine.substring(0, 20)}" 后="${nextLine.substring(0, 20)}"`);
continue;
}
}
result.push(line);
}
return { text: result.join('\n'), processed: true };
}
/**
* 清理编辑器中的列表空行(处理 Obsidian 内部复制的情况)
*/
cleanupEditorLists(editor) {
const cursor = editor.getCursor();
const line = cursor.line;
// 扫描范围:光标前20行到光标后5行
const startLine = Math.max(0, line - 20);
const endLine = Math.min(editor.lineCount() - 1, line + 5);
let removedCount = 0;
// 从后往前删除,避免行号变化
for (let i = endLine; i >= startLine + 1; i--) {
// 边界检查
if (i < 1 || i >= editor.lineCount()) continue;
const currentLine = editor.getLine(i);
const prevLine = editor.getLine(i - 1);
const nextLine = i < editor.lineCount() - 1 ? editor.getLine(i + 1) : null;
const isPrevList = /^\s*[-*+]\s/.test(prevLine);
const isCurrentBlank = currentLine.trim() === '';
const isNextList = nextLine ? /^\s*[-*+]\s/.test(nextLine) : false;
// 删除列表项之间的空行
if (isPrevList && isCurrentBlank && isNextList) {
editor.replaceRange('',
{ line: i, ch: 0 },
{ line: i + 1, ch: 0 }
);
removedCount++;
this.log(`删除第 ${i + 1} 行的空行`);
}
}
if (removedCount > 0) {
this.log(`共删除 ${removedCount} 个列表空行`);
}
return removedCount;
}
/**
* 条件日志输出(仅在调试模式下)
*/
log(...args) {
if (this.settings.debugMode) {
console.log('[粘贴优化]', ...args);
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
onunload() {
this.log('粘贴优化插件已卸载');
}
}
/**
* 设置页面
*/
class PasteOptimizerSettingTab extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: '粘贴优化设置' });
// 功能开关
new obsidian.Setting(containerEl)
.setName('去除列表空行')
.setDesc('自动去除粘贴时列表项之间的空行')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.removeListBlankLines)
.onChange(async (value) => {
this.plugin.settings.removeListBlankLines = value;
await this.plugin.saveSettings();
}));
new obsidian.Setting(containerEl)
.setName('清理 Books 版权信息')
.setDesc('自动清理从 Apple Books 复制的版权声明')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.cleanBooksCopyright)
.onChange(async (value) => {
this.plugin.settings.cleanBooksCopyright = value;
await this.plugin.saveSettings();
}));
new obsidian.Setting(containerEl)
.setName('自动添加列表格式')
.setDesc('清理 Books 内容时,自动为每行添加列表标记(- )。关闭此选项将保留原文段落格式')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoAddListFormat)
.onChange(async (value) => {
this.plugin.settings.autoAddListFormat = value;
await this.plugin.saveSettings();
}));
// 高级设置
containerEl.createEl('h3', { text: '高级设置' });
new obsidian.Setting(containerEl)
.setName('调试模式')
.setDesc('在控制台输出详细的调试信息(开发者选项)')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.debugMode)
.onChange(async (value) => {
this.plugin.settings.debugMode = value;
await this.plugin.saveSettings();
}));
// 使用说明
containerEl.createEl('h3', { text: '使用说明' });
const descEl = containerEl.createDiv();
descEl.innerHTML = `
<p><strong>自动功能:</strong></p>
<ul>
<li>粘贴时自动处理(根据上述设置)</li>
</ul>
<p><strong>手动命令(Ctrl/Cmd + P):</strong></p>
<ul>
<li><code>去除列表项之间的空行</code> - 处理选中文本或整个文档</li>
<li><code>清理当前列表的空行</code> - 清理光标所在的列表</li>
<li><code>清理 Books 版权信息</code> - 清理选中的 Books 内容</li>
</ul>
<p><strong>提示:</strong></p>
<ul>
<li>从 Books 复制时会自动清理版权信息和引号</li>
<li>从 Obsidian 内部复制列表时会自动去除空行</li>
<li>如遇问题,可开启调试模式查看详细日志</li>
</ul>
`;
}
}
module.exports = PasteOptimizerPlugin;manifest.json
{
"id": "paste-optimizer",
"name": "Paste Optimizer",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "优化粘贴体验:去除列表空行、清理 Books 版权信息",
"author": "jz",
"authorUrl": "",
"isDesktopOnly": false
}