-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.js
502 lines (438 loc) · 17.8 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
'use strict';
var obsidian = require('obsidian');
// 默认设置
const DEFAULT_SETTINGS = {
autoConvert: true,
blogFolders: [], // 默认为空数组
linkFormat: '@/blog/{}.md', // 默认链接格式,{} 将被替换为文件名
showSuccessNotice: true // 默认显示成功通知
};
// 文本国际化
const TEXTS = {
'plugin_name': {
en: 'Wiki Link Converter Settings',
zh: 'Wiki链接转换器设置'
},
'link_format': {
en: 'Link Format',
zh: '链接格式'
},
'link_format_desc': {
en: 'Set the format of the converted link. e.g. @/blog/{}.md will generate [filename](@/blog/filename.md)',
zh: '设置转换后的链接格式。例如:@/blog/{}.md 会生成 [文件名](@/blog/文件名.md)'
},
'auto_convert': {
en: 'Auto Convert',
zh: '自动转换'
},
'auto_convert_desc': {
en: 'Automatically converts Wiki links in the monitor folder when editing.',
zh: '编辑时自动转换监控文件夹中的Wiki链接。'
},
'monitor_folders': {
en: 'Monitor Folders',
zh: '监控文件夹'
},
'monitor_folders_desc': {
en: 'Folders to monitor for auto-conversion. (use / for entire vault, you can add multiple folders)',
zh: '需要被监控的文件夹。(使用 / 表示整个仓库,可添加多个文件夹)'
},
'add_folder': {
en: 'Add Folder',
zh: '添加文件夹'
},
'delete': {
en: 'Delete',
zh: '删除'
},
'convert_command': {
en: 'Convert Wiki Links to Custom Format',
zh: '转换Wiki链接为自定义格式'
},
'convert_menu': {
en: 'Convert Wiki Links',
zh: '转换Wiki链接'
},
'convert_success': {
en: 'Wiki links converted successfully',
zh: 'Wiki链接转换成功'
},
'convert_file_success': {
en: 'Converted links in {}',
zh: '已转换 {} 中的链接'
},
'convert_error': {
en: 'Error converting {}',
zh: '转换 {} 时出错'
},
'format_error': {
en: 'Must include {} as filename placeholder',
zh: '必须包含 {} 作为文件名占位符'
},
'show_notice': {
en: 'Show Success Notice',
zh: '显示成功通知'
},
'show_notice_desc': {
en: 'Show notification when links are converted successfully.',
zh: '链接转换成功时显示通知。'
}
};
class WikiLinkConverterPlugin extends obsidian.Plugin {
settings;
isZhLanguage = false;
// 获取国际化文本
getText(key, ...args) {
const text = TEXTS[key][this.isZhLanguage ? 'zh' : 'en'];
return args.length ? text.replace(/\{\}/g, () => args.shift()) : text;
}
async onload() {
await this.loadSettings();
// 检测语言
this.isZhLanguage = document.documentElement.lang === 'zh';
// 添加设置选项卡
this.addSettingTab(new WikiLinkConverterSettingTab(this.app, this));
// 添加命令
this.addCommand({
id: 'convert-wiki-links',
name: this.getText('convert_command'),
editorCallback: (editor, view) => {
this.convertWikiLinks(editor);
}
});
// 注册自动转换事件处理器
this.registerAutoConvert();
// 添加右键菜单
this.registerEvent(
this.app.workspace.on('file-menu', (menu, file) => {
// 只对 markdown 文件显示菜单
if (file && file.extension === 'md') {
menu.addItem((item) => {
item
.setTitle(this.getText('convert_menu'))
.setIcon('link')
.onClick(async () => {
await this.convertLinksInFile(file);
});
});
}
})
);
}
// 统一的链接转换逻辑
convertWikiLinksInText(text) {
return text.replace(/\[\[(.*?)\]\]/g, (match, fileName) => {
// 如果链接内容为空,保持原样
if (!fileName.trim()) {
return match;
}
// 使用自定义格式转换链接
const link = this.settings.linkFormat.replace('{}', fileName);
return `[${fileName}](${link})`;
});
}
// 编辑器中的转换
convertWikiLinks(editor) {
const content = editor.getValue();
const newContent = this.convertWikiLinksInText(content);
if (content !== newContent) {
// 保存当前光标位置
const cursor = editor.getCursor();
// 计算光标位置的偏移量
const beforeCursor = content.slice(0, editor.posToOffset(cursor));
const afterConversion = this.convertWikiLinksInText(beforeCursor);
const offset = afterConversion.length - beforeCursor.length;
// 更新内容
editor.setValue(newContent);
// 恢复光标位置,考虑转换后的偏移
const newPos = editor.offsetToPos(editor.posToOffset(cursor) + offset);
editor.setCursor(newPos);
this.showNotice('convert_success');
}
}
handleEditorChange(editor) {
// 获取当前编辑的文件
const file = this.app.workspace.getActiveFile();
// 快速检查:如果不是markdown文件直接返回
if (file?.extension !== 'md') return;
// 检查文件是否在监控的文件夹中
if (!this.isInBlogFolders(file)) return;
// 获取当前光标位置和行内容
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
// 检查当前行是否包含完整的Wiki链接
const wikiLinkRegex = /\[\[.*?\]\]/;
if (!wikiLinkRegex.test(line)) return;
this.convertWikiLinks(editor);
}
onunload() {
console.log('卸载 Wiki Link Converter 插件');
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
// 确保 blogFolders 是数组
if (!Array.isArray(this.settings.blogFolders)) {
this.settings.blogFolders = [this.settings.blogFolders].filter(Boolean);
}
}
async saveSettings() {
await this.saveData(this.settings);
}
// 检查文件是否在文件夹中
isInBlogFolders(file) {
// 如果包含根目录"/",则处理所有markdown文件
if (this.settings.blogFolders.includes('/')) return true;
// 检查文件是否在任一指定文件夹中
const filePath = file.path;
return this.settings.blogFolders.some(folder =>
filePath.startsWith(folder + '/') || filePath === folder
);
}
// 文件中的转换
async convertLinksInFile(file) {
try {
const content = await this.app.vault.read(file);
const newContent = this.convertWikiLinksInText(content);
if (content !== newContent) {
await this.app.vault.modify(file, newContent);
this.showNotice('convert_file_success', file.basename);
}
} catch (error) {
console.error('转换链接时出错:', error);
this.showNotice('convert_error', file.basename);
}
}
// 注册自动转换事件处理器
registerAutoConvert() {
// 如果已经注册过,先移除旧的
if (this.autoConvertHandler) {
this.app.workspace.off('editor-change', this.autoConvertHandler);
this.autoConvertHandler = null;
}
// 如果启用了自动转换,注册新的处理器
if (this.settings.autoConvert) {
this.autoConvertHandler = (editor) => this.handleEditorChange(editor);
this.registerEvent(
this.app.workspace.on('editor-change', this.autoConvertHandler)
);
}
}
// 修改通知方法
showNotice(key, ...args) {
// 只有convert_success受开关控制
if (key === 'convert_success' && !this.settings.showSuccessNotice) return;
new obsidian.Notice(this.getText(key, ...args));
}
}
class WikiLinkConverterSettingTab extends obsidian.PluginSettingTab {
plugin;
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
let {containerEl} = this;
containerEl.empty();
containerEl.createEl('h2', {text: this.plugin.getText('plugin_name')});
// 链接格式设置
new obsidian.Setting(containerEl)
.setName(this.plugin.getText('link_format'))
.setDesc(this.plugin.getText('link_format_desc'))
.addText(text => {
const settingControl = text.inputEl.parentElement;
settingControl.style.position = 'relative'; // 添加相对定位
text.setPlaceholder('@/blog/{}.md')
.setValue(this.plugin.settings.linkFormat);
text.inputEl.style.width = '200px';
// 创建错误信息元素
const errorMessage = document.createElement('div');
errorMessage.className = 'format-error';
errorMessage.style.display = 'none';
errorMessage.textContent = this.plugin.getText('format_error');
settingControl.appendChild(errorMessage);
// 添加失焦事件监听
text.inputEl.addEventListener('blur', async () => {
const value = text.getValue();
errorMessage.style.display = !value.includes('{}') ? 'block' : 'none';
});
// 添加获得焦点事件监听
text.inputEl.addEventListener('focus', () => {
errorMessage.style.display = 'none';
});
// 值变更事件
text.onChange(async (value) => {
this.plugin.settings.linkFormat = value;
await this.plugin.saveSettings();
});
});
// 显示成功通知设置
new obsidian.Setting(containerEl)
.setName(this.plugin.getText('show_notice'))
.setDesc(this.plugin.getText('show_notice_desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showSuccessNotice)
.onChange(async (value) => {
this.plugin.settings.showSuccessNotice = value;
await this.plugin.saveSettings();
}));
// 自动转换设置
new obsidian.Setting(containerEl)
.setName(this.plugin.getText('auto_convert'))
.setDesc(this.plugin.getText('auto_convert_desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoConvert)
.onChange(async (value) => {
this.plugin.settings.autoConvert = value;
await this.plugin.saveSettings();
// 重新注册事件处理器
this.plugin.registerAutoConvert();
}));
// 文件夹设置
const blogFolderSetting = new obsidian.Setting(containerEl)
.setName(this.plugin.getText('monitor_folders'))
.setDesc(this.plugin.getText('monitor_folders_desc'))
.addButton(button => button
.setButtonText(this.plugin.getText('add_folder'))
.onClick(async () => {
if (!this.plugin.settings.blogFolders) {
this.plugin.settings.blogFolders = [];
}
this.plugin.settings.blogFolders.push('');
await this.plugin.saveSettings();
this.display();
}));
// 文件夹列表容器
const folderListContainer = containerEl.createDiv('setting-item-children');
folderListContainer.style.paddingLeft = '40px';
// 获取所有文件夹
const folders = this.getAllFolders();
// 显示现有的文件夹
if (this.plugin.settings.blogFolders && this.plugin.settings.blogFolders.length > 0) {
this.plugin.settings.blogFolders.forEach((folder, index) => {
const folderSetting = new obsidian.Setting(folderListContainer)
.addText(text => text
.setPlaceholder('/')
.setValue(folder)
.onChange(async (value) => {
this.plugin.settings.blogFolders[index] = value;
await this.plugin.saveSettings();
}))
.addButton(button => button
.setIcon('trash')
.setTooltip(this.plugin.getText('delete'))
.onClick(async () => {
this.plugin.settings.blogFolders.splice(index, 1);
await this.plugin.saveSettings();
this.display();
}));
// 添加文件夹自动补全
const textInput = folderSetting.components[0];
const inputEl = textInput.inputEl;
const inputContainer = inputEl.parentElement;
// 创建一个新的容器来包裹输入框和下拉菜单
const wrapperDiv = document.createElement('div');
wrapperDiv.style.position = 'relative';
inputEl.parentElement.appendChild(wrapperDiv);
wrapperDiv.appendChild(inputEl);
// 创建下拉菜单容器
const dropdown = wrapperDiv.createDiv('suggestion-dropdown');
dropdown.style.display = 'none';
// 显示建议
const showSuggestions = () => {
const value = inputEl.value.toLowerCase();
// 根据输入内容过滤文件夹
const matches = value === '' ?
folders : // 如果输入为空,显示所有文件夹
folders.filter(f => f.toLowerCase().includes(value)); // 否则只显示匹配的文件夹
if (matches.length > 0) {
dropdown.empty();
dropdown.style.display = 'block';
matches.forEach(match => {
const item = dropdown.createDiv('suggestion-item');
item.setText(match);
item.onClickEvent(() => {
textInput.setValue(match);
dropdown.style.display = 'none';
this.plugin.settings.blogFolders[index] = match;
this.plugin.saveSettings();
});
});
} else {
dropdown.style.display = 'none';
}
};
// 输入事件
inputEl.addEventListener('input', showSuggestions);
// 获得焦点时立即显示所有文件夹
inputEl.addEventListener('focus', () => {
showSuggestions();
});
// 失去焦点时延迟隐藏下拉菜单
inputEl.addEventListener('blur', () => {
setTimeout(() => {
dropdown.style.display = 'none';
}, 200);
});
});
}
// 添加样式
const style = containerEl.createEl('style');
style.textContent = `
.setting-item-children {
margin-top: 8px;
}
.setting-item-children .setting-item {
border: none;
padding-top: 8px;
}
.suggestion-dropdown {
position: absolute;
top: 32px;
left: 0;
width: 200px;
max-height: 200px;
overflow-y: auto;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
box-shadow: 0 2px 8px var(--background-modifier-box-shadow);
z-index: 1000;
}
.suggestion-item {
padding: 6px 8px;
cursor: pointer;
text-align: left;
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.suggestion-item:hover {
background-color: var(--background-modifier-hover);
}
.setting-item input[type='text'] {
width: 200px;
}
.format-error {
position: absolute;
top: 30%;
right: 220px;
transform: translateY(-50%);
color: var(--text-error);
font-size: 12px;
white-space: nowrap;
z-index: 1;
}
`;
}
getAllFolders() {
const folders = new Set(['/']); // 添加根目录选项
this.app.vault.getAllLoadedFiles().forEach(file => {
if (file.parent) {
folders.add(file.parent.path);
}
});
return Array.from(folders).sort();
}
}
module.exports = WikiLinkConverterPlugin;
/* nosourcemap */