感觉自己很牛逼!!! 为啥想到写这个插件呢? 因为我想把Obsidian和Hugo结合起来,但是有两个小问题很麻烦,虽然很小,但用多了会很烦!会写代码的一般都很懒,所以绝对不会干出手动复制粘贴这种蠢事的。 问题有两个,先解决其中一个:
- markdown的
==
表示的高亮无法识别,使用html的mark标识可以,但是我懒得打<>
这个括号,双击等于号多快啊。 所以我就想到了,写一个脚本来转换
一开始想到的是Python脚本,在每次上传的时候自动遍历文件修改一下,但转念一想,这多慢啊,直接Obsidian插件吧,实时识别和更换,应该就是正则匹配就行了。
于是,就有了下面的实现:
import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
/**
* 替换 Markdown 内容中的 ==xxx== 为 <mark>xxx</mark>,但忽略代码块和行内代码中的 ==。
* 支持 ``` 和 ~~~ 作为代码块标记。
* @param content 原始 Markdown 内容
* @returns 替换后的 Markdown 内容
*/
function replaceHighlight(content: string): string {
const lines = content.split('\n');
let inCodeBlock = false;
let codeBlockDelimiter = ''; // 用于记录当前代码块的分隔符(``` 或 ~~~)
const replacedLines = lines.map(line => {
// 检查代码块开始或结束
const codeBlockMatch = line.match(/^(```|~~~)/);
if (codeBlockMatch) {
if (!inCodeBlock) {
inCodeBlock = true;
codeBlockDelimiter = codeBlockMatch[1];
} else if (codeBlockMatch[1] === codeBlockDelimiter) {
inCodeBlock = false;
codeBlockDelimiter = '';
}
return line;
}
if (inCodeBlock) {
// 处于代码块中,跳过替换
return line;
} else {
// 处理行内代码
// 暂时保护行内代码中的 ==,避免替换
const inlineCodeRegex = /`([^`]+)`/g;
let protectedLine = line;
const inlineCodes: string[] = [];
let match;
while ((match = inlineCodeRegex.exec(line)) !== null) {
inlineCodes.push(match[1]);
protectedLine = protectedLine.replace(match[0], `__INLINE_CODE_${inlineCodes.length - 1}__`);
}
// 替换 ==xxx== 为 <mark>xxx</mark>
protectedLine = protectedLine.replace(/==(.+?)==/g, '<mark>$1</mark>');
// 恢复行内代码
protectedLine = protectedLine.replace(/__INLINE_CODE_(\d+)__/g, (m, p1) => {
const index = parseInt(p1);
return `\`${inlineCodes[index]}\``;
});
return protectedLine;
}
});
return replacedLines.join('\n');
}
export default class Mark2HighlightPlugin extends Plugin {
private onEditHandler: () => void;
async onload() {
// 添加一个命令用于高亮替换
this.addCommand({
id: 'convert-highlight',
name: 'Convert ==xxx== to <mark>xxx</mark>',
editorCallback: (editor: Editor, view: MarkdownView) => {
// 获取编辑器内容
const content = editor.getValue();
// 使用辅助函数替换 ==xxx== 为 <mark>xxx</mark>
const updatedContent = replaceHighlight(content);
// 更新编辑器内容
editor.setValue(updatedContent);
new Notice('✨ Markdown Highlight Converted!');
}
});
// 自动监听编辑器变化并进行替换
this.onEditHandler = () => {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {
const editor = activeView.editor;
const content = editor.getValue();
const updatedContent = replaceHighlight(content);
if (content !== updatedContent) {
editor.setValue(updatedContent);
new Notice('✨ Markdown Highlight Converted Automatically!');
}
}
};
this.registerEvent(this.app.workspace.on('editor-change', this.onEditHandler));
// 如果不需要设置选项卡,可以移除以下行
// this.addSettingTab(new Mark2HighlightSettingTab(this.app, this));
}
onunload() {
console.log('Mark to Highlight Plugin Unloaded.');
}
}
具体而言,是在基础的sample上面进行修改的:
npx degit obsidianmd/obsidian-sample-plugin my-plugin
cd my-plugin
npm install
修改完ts
文件后使用:
npm run build
想到了下一个插件,就是中英字符的自动切换,描述如下: 比如输入》空格,或者〉空格,就是自动转成> 比如输入·空格,或者···空格,就自动转成makrdown下的代码框标识
于是就有了下面这个:
import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
/**
* 插件的设置接口
*/
interface AutoConvertCharsPluginSettings {
autoConvertDelay: number; // 自动转换的延迟时间(毫秒)
}
/**
* 插件的默认设置
*/
const DEFAULT_SETTINGS: AutoConvertCharsPluginSettings = {
autoConvertDelay: 100
};
/**
* 判断指定行是否在代码块内
* @param lines 所有行内容
* @param lineNumber 当前行号(从0开始)
* @returns 是否在代码块内
*/
function isLineInCodeBlock(lines: string[], lineNumber: number): boolean {
let inCodeBlock = false;
let codeBlockDelimiter = '';
for (let i = 0; i <= lineNumber; i++) {
const line = lines[i];
const codeBlockMatch = line.match(/^(```|~~~)/);
if (codeBlockMatch) {
if (!inCodeBlock) {
inCodeBlock = true;
codeBlockDelimiter = codeBlockMatch[1];
} else if (codeBlockMatch[1] === codeBlockDelimiter) {
inCodeBlock = false;
codeBlockDelimiter = '';
}
}
}
return inCodeBlock;
}
/**
* 替换单行中的特定字符组合为目标符号,忽略行内代码中的匹配
* @param line 原始行内容
* @returns 替换后的行内容及是否进行了替换
*/
function replacePatternsInLine(line: string): { newLine: string; replaced: boolean } {
// **重要**:将更长的触发字符组合放在前面,避免部分匹配
const patterns = [
{ trigger: '··· ', replacement: '``````' }, // 六个反引号
{ trigger: '· ', replacement: '``' }, // 两个反引号
{ trigger: '》 ', replacement: '>' }, // 引用符号
{ trigger: '〉 ', replacement: '>' } // 引用符号
];
let inInlineCode = false;
let result = '';
let replaced = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
// 处理行内代码的切换
if (char === '`') {
inInlineCode = !inInlineCode;
result += char;
continue;
}
if (!inInlineCode) {
let matched = false;
for (const pattern of patterns) {
const triggerLength = pattern.trigger.length;
const triggerSubstring = line.substring(i, i + triggerLength);
if (triggerSubstring === pattern.trigger) {
result += pattern.replacement;
i += triggerLength - 1; // 跳过触发字符
replaced = true;
matched = true;
break; // 跳出当前循环,继续下一个字符
}
}
if (matched) {
continue; // 已经替换,跳过添加当前字符
}
}
result += char;
}
return { newLine: result, replaced };
}
/**
* 简单的防抖函数实现
* @param func 需要防抖的函数
* @param wait 防抖的时间间隔(毫秒)
* @returns 防抖后的函数
*/
function debounceFunc(func: () => void, wait: number) {
let timeout: number | undefined;
return () => {
if (timeout !== undefined) {
clearTimeout(timeout);
}
timeout = window.setTimeout(() => {
func();
}, wait);
};
}
export default class AutoConvertCharsPlugin extends Plugin {
settings: AutoConvertCharsPluginSettings;
private onEditHandler: () => void;
/**
* 加载插件时的初始化逻辑
*/
async onload() {
await this.loadSettings();
// 创建防抖后的编辑器变化处理函数
this.onEditHandler = debounceFunc(() => {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {
const editor = activeView.editor;
this.convertPatterns(editor);
}
}, this.settings.autoConvertDelay);
// 注册编辑器变化事件
this.registerEvent(this.app.workspace.on('editor-change', this.onEditHandler));
// 添加设置选项卡
this.addSettingTab(new AutoConvertCharsSettingTab(this.app, this));
}
/**
* 卸载插件时的清理逻辑
*/
onunload() {
console.log('Auto Convert Chars Plugin Unloaded.');
}
/**
* 加载插件设置
*/
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
/**
* 保存插件设置
*/
async saveSettings() {
await this.saveData(this.settings);
}
/**
* 更新自动转换延迟时间
* @param newDelay 新的延迟时间(毫秒)
*/
public updateAutoConvertDelay(newDelay: number) {
this.settings.autoConvertDelay = newDelay;
this.saveSettings();
// 移除旧的事件监听
this.app.workspace.off('editor-change', this.onEditHandler);
// 创建新的防抖处理函数
this.onEditHandler = debounceFunc(() => {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {
const editor = activeView.editor;
this.convertPatterns(editor);
}
}, this.settings.autoConvertDelay);
// 重新注册编辑器变化事件
this.registerEvent(this.app.workspace.on('editor-change', this.onEditHandler));
}
/**
* 执行特定字符组合的自动转换,并保持光标位置
* @param editor 当前编辑器实例
*/
private convertPatterns(editor: Editor) {
const cursor = editor.getCursor(); // 保存光标位置
const currentLineNumber = cursor.line;
const currentLineText = editor.getLine(currentLineNumber);
// 获取所有行内容
const allLines = editor.getValue().split('\n');
// 判断当前行是否在代码块内
const inCodeBlock = isLineInCodeBlock(allLines, currentLineNumber);
if (inCodeBlock) {
// 当前行在代码块内,跳过替换
return;
}
// 获取替换后的行内容
const { newLine, replaced } = replacePatternsInLine(currentLineText);
if (replaced) {
// 获取光标在行内的位置
const beforeCursorText = currentLineText.substring(0, cursor.ch);
// 计算替换前触发字符的位置和长度
let triggerStart = -1;
let triggerLength = 0;
let matchedPattern: { trigger: string; replacement: string } | null = null;
const patterns = [
{ trigger: '··· ', replacement: '``````' }, // 六个反引号
{ trigger: '· ', replacement: '``' }, // 两个反引号
{ trigger: '》 ', replacement: '>' }, // 引用符号
{ trigger: '〉 ', replacement: '>' } // 引用符号
];
for (const pattern of patterns) {
const index = beforeCursorText.lastIndexOf(pattern.trigger);
if (index !== -1) {
triggerStart = index;
triggerLength = pattern.trigger.length;
matchedPattern = pattern;
break;
}
}
if (triggerStart !== -1 && matchedPattern !== null) {
// 定义替换范围
const from = { line: currentLineNumber, ch: triggerStart };
const to = { line: currentLineNumber, ch: triggerStart + triggerLength };
// 定义替换内容
const replacement = matchedPattern.replacement;
if (replacement) {
// 执行替换
editor.replaceRange(replacement, from, to);
// 设置光标位置到替换后的中间
// 对于 '· ' → '`` ',光标应在两个反引号之间,即位置 1
// 对于 '··· ' → '`````` ',光标应在前三个反引号之后,即位置 3
let newCursorCh: number;
if (matchedPattern.trigger === '· ') {
newCursorCh = triggerStart + 1; // After first backtick
} else if (matchedPattern.trigger === '··· ') {
newCursorCh = triggerStart + 3; // After third backtick
} else {
// For other patterns like '》 ' and '〉 ', place cursor after replacement
newCursorCh = triggerStart + replacement.length;
}
editor.setCursor({ line: currentLineNumber, ch: newCursorCh });
new Notice('✨ Auto Convert Markdown Chars ✨');
}
}
}
}
}
/**
* 插件的设置选项卡
*/
class AutoConvertCharsSettingTab extends PluginSettingTab {
plugin: AutoConvertCharsPlugin;
constructor(app: App, plugin: AutoConvertCharsPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// 设置标题
containerEl.createEl('h2', { text: 'Auto Convert Chars 设置' });
// 自动识别延迟设置
new Setting(containerEl)
.setName('自动转换延迟(毫秒)')
.setDesc('设置自动识别并转换特定字符组合的延迟时间,默认100毫秒。')
.addText(text => text
.setPlaceholder('100')
.setValue(this.plugin.settings.autoConvertDelay.toString())
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed >= 0) {
// 调用插件的公共方法更新延迟时间
this.plugin.updateAutoConvertDelay(parsed);
new Notice(`自动转换延迟已设置为 ${parsed} 毫秒`);
} else {
new Notice('请输入有效的毫秒数(非负整数)。');
}
}));
}
}