其他
其他
官方示例
Obsidian-sample-plugin
https://github.com/obsidianmd/obsidian-sample-plugin
使用
编译方法
# 首先要先安装nodejs,里面包含npm工具
npm i # 等同于npm install
npm run dev # 进行编译,将ts编译成js
# 如果Obsidian API更新了
npm update
手动安装插件
复制 main.js, styles.css, manifest. js
到Ob的插件路径
功能
- 有设置菜单
- 有小图标,小图标可以输出tips
- 输出文字 “Sample Editor Command”
- 打开提示窗
发布新版本
json加上你的版本号,以及最新版本所需的最低Obsidian版本
更新你的版本
上传清单:Json,main.js,styles.css。
注意: Json文件必须在两个地方,第一个是存储库的根路径,另一个是发布版本你可以在手动更新 manifest.json 中的 minAppVersion
运行:npm version patch # 或 npm version minor # 或 npm version major
来来简化版本碰撞过程
将您的插件添加到社区插件列表中
略
使用eslint提高代码质量(可选)
略
代码
main.ts(注释汉化版本)
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// 记得重命名这些类和接口!
interface MyPluginSettings {
mySetting: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
async onload() {
await this.loadSettings();
// 这将在左侧色带中创建一个图标
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// 当用户单击图标时调用:发送一条信息
new Notice('This is a notice!');
});
// 对色带执行额外的操作
ribbonIconEl.addClass('my-plugin-ribbon-class');
// 这将添加一个状态栏项目的底部的应用程序。不工作在移动应用程序
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// 这增加了一个可以在任何地方触发的简单命令
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// 这将添加一个编辑器命令,可以对当前编辑器实例执行某些操作
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// 这添加了一个复杂的命令,可以检查应用程序的当前状态是否允许执行命令
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// 条件检查
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// 如果检查为真,我们只是在“检查”命令是否可以运行
// 如果检查为false,那么我们想实际执行操作
if (!checking) {
new SampleModal(this.app).open();
}
// 只有当check函数返回true时,该命令才会显示在命令面板中
return true;
}
}
});
// 这将添加一个设置选项卡,以便用户可以配置插件的各个方面
this.addSettingTab(new SampleSettingTab(this.app, this));
// 如果该插件连接了任何全局DOM事件(在不属于该插件的应用程序部分上)
//当这个插件被禁用时,使用这个函数会自动删除事件监听器。
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// 当注册间隔时,当插件被禁用时,该函数将自动清除间隔
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
console.log('Secret: ' + value);
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}
API接口
https://github.com/obsidianmd/obsidian-api
也可以看开发文档中,接口类自动生成的文档(但是没有多什么注释,感觉和直接看api文件也差不多):
https://luhaifeng666.github.io/obsidian-plugin-docs-zh/zh2.0/reference/typescript/classes/AbstractTextComponent.html
obsidian.d.ts
// 4406行
插入链接示例(开发文档示例)
https://luhaifeng666.github.io/obsidian-plugin-docs-zh/zh2.0/examples/insert-link.html
main.ts
import { Editor, Plugin } from "obsidian";
import { InsertLinkModal } from "./modal";
export default class InsertLinkPlugin extends Plugin {
async onload() {
this.addCommand({
id: "insert-link",
name: "Insert link",
editorCallback: (editor: Editor) => {
const selectedText = editor.getSelection();
const onSubmit = (text: string, url: string) => {
editor.replaceSelection(`[${text}](${url})`);
};
new InsertLinkModal(this.app, selectedText, onSubmit).open();
},
});
}
}
./modal.ts
import { App, Modal, Setting } from "obsidian";
export class InsertLinkModal extends Modal {
linkText: string;
linkUrl: string;
onSubmit: (linkText: string, linkUrl: string) => void;
constructor(
app: App,
defaultLinkText: string,
onSubmit: (linkText: string, linkUrl: string) => void
) {
super(app);
this.linkText = defaultLinkText;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h1", { text: "Insert link" });
new Setting(contentEl).setName("Link text").addText((text) =>
text.setValue(this.linkText).onChange((value) => {
this.linkText = value;
})
);
new Setting(contentEl).setName("Link URL").addText((text) =>
text.setValue(this.linkUrl).onChange((value) => {
this.linkUrl = value;
})
);
new Setting(contentEl).addButton((btn) =>
btn
.setButtonText("Insert")
.setCta()
.onClick(() => {
this.close();
this.onSubmit(this.linkText, this.linkUrl);
})
);
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
试作插件 - 切换复选框状态
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import test from 'node:test';
// 记得重命名这些类和接口!
interface MyPluginSettings {
mySetting: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
/* 1. Plugin接口的实现
*
*/
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
// 插件加载后
async onload() {
await this.loadSettings();
/* 1. 左侧工具栏相关的操作 */
/* 在左侧色带中创建一个图标
* 参数1:估计是图标名
* 参数2:悬浮提示名
* 参数3:事件
*/
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// 当用户单击图标时调用:发送一条信息
new Notice('This is a notice!');
});
// 对色带执行额外的操作
ribbonIconEl.addClass('my-plugin-ribbon-class');
/* 2. 右下角的状态栏增添一段文字
* 移动应用程序中不工作
*/
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
/* 3. 下面增加三个可以在任何地方触发的简单命令
* addCommand函数
* 参数1:id
* 参数2:命令名字
* 参数3:事件,一个回调函数
*/
// 3.1. 打开一个窗口
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// 3.2. 这将添加一个编辑器命令,可以对当前编辑器实例执行某些操作。这里是在光标所在位置打印一串字符串
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// 3.3. 这添加了一个复杂的命令,可以检查应用程序的当前状态是否允许执行命令
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// 条件检查
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// 如果检查为真,我们只是在“检查”命令是否可以运行
// 如果检查为false,那么我们想实际执行操作
if (!checking) {
new SampleModal(this.app).open();
}
// 只有当check函数返回true时,该命令才会显示在命令面板中
return true;
}
}
});
// 一些实验:
// editor.getValue() // 获取全部内容
// editor.lineCount() // 一共有多少行
// let i = editor.getCursor().line // 光标在第几行
// editor.replaceSelection('SSS:'+s1+"DDD") // 光标后输出
// "- [ ] 678".match(new RegExp(`^(\s*- )(\\[[* +-x]\\])(.*)`))
// Array(4) [ "- [ ] 678", "- ", "[ ]", " 678" ]
// editor.replaceSelection(' SSS:【'+ s1.match(r1)+"】【" +s1.match(r2)+"】【"+ s1+"】")
// 4. 切换鼠标所在行的复选框状态
this.addCommand({
id: 'test',
name: '切换鼠标所在行的复选框状态',
editorCallback: (editor: Editor, view: MarkdownView) => {
let s1 :string = editor.getLine(editor.getCursor().line) // 获取序列行的内容
let r1 = new RegExp(`^\\s*- \\[[( )]\\](.*)`)
let r2 = new RegExp(`^\\s*- \\[[(*+-x)]\\](.*)`)
if (s1.match(r1)) {
s1 = s1.replace("[ ]","[*]") // 这里写得比较简陋,会有bug,比如- [ ] [ ]
}
else if (s1.match(r2)) {
s1 = s1.replace("[*]","[ ]")
}
editor.setLine(editor.getCursor().line, s1)
}
});
// 这将添加一个设置选项卡,以便用户可以配置插件的各个方面
this.addSettingTab(new SampleSettingTab(this.app, this));
// 如果该插件连接了任何全局DOM事件(在不属于该插件的应用程序部分上)
//当这个插件被禁用时,使用这个函数会自动删除事件监听器。
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// 当注册间隔时,当插件被禁用时,该函数将自动清除间隔
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
// 插件停用后
onunload() {
}
// 插件设置加载后
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
// 插件设置保存后
async saveSettings() {
await this.saveData(this.settings);
}
}
/* 2. Modal接口的实现
* 打开的提示窗口的实体
*/
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
/* 3. PluginSettingTab接口的实现
* 这个是设置菜单里的设置
*/
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
// 一个h2标题
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
// 创建一个新的设置项
new Setting(containerEl)
.setName('Setting #1') // 设置项名字
.setDesc('It\'s a secret') // 设置项提示
.addText(text => text // 输入框
.setPlaceholder('Enter your secret') // 没有内容时的提示
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
console.log('Secret: ' + value);
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}
后处理器
https://luhaifeng666.github.io/obsidian-plugin-docs-zh/zh2.0/editor/markdown-post-processing.html
使用:例如:(预览模式下生效)
`:+1:`
其他
直接让Obsidian界面半崩溃
// app = new App()
Vault
js文件都是在一个虚拟路径上运行的:
C:\Users\A\AppData\Local\Obsidian\resources\electron.asar
英文状态下,打开其他库叫 “Open another vault”
打开其他库的菜单中,显示的是:Create new vault、OPen folder as vault、Open vault form Obsidian Sync
控制台可以直接打印:(比较方便)
__filename
this
this.app 或 app
this.app.vault # .adapter.basePath就是库的路径了(包括库名)
# .configDir
# .getName(),当前库的名字
# _.sourcePath
view / editor
好多叫view/editor的,他们的类型都不同
我个人认为将诸如MarkdownView、EditorView、View的变量都叫成view,是非常不妥的
// 受参类
this.addCommand({
id: '',
name: '',
editorCallback: (
editor: Editor, // Editor
view: MarkdownView // MarkdownView
) => {
// @ts-expect-error, not typed
const editorView = view.editor.cm as EditorView;// EditorView from "@codemirror/view"
const plugin = editorView.plugin(examplePlugin);
});
// 受参类
export class AnyBlockPluginValue implements PluginValue {
decorations: DecorationSet;
constructor(view: EditorView) { view } // EditorView from "@codemirror/view"
update(update: ViewUpdate) { update.view } // 这个view和上面那个是一样的
}
const view: View|null = this.app.workspace.getActiveViewOfType(MarkdownView);// // 未聚焦(active)会返回null
if (!view) return false // 不在编辑模式好像**不**会返回null
// @ts-ignore 这里会说View没有editor属性
const editor: Editor = view.editor
// @ts-ignore 这里会说Editor没有cm属性
const editorView: EditorView = editor.cm
// @ts-ignore 这里会说Editor没有containerEl属性
const editorElement: Element = editVar.editor.containerEl
const editorState: EditorState = editorView.state
// const state: any = view.getState() 这个不是
const cursor: EditorPosition = editor.getCursor();
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
this
plugin_this
if (!view) return false
const editor = view.editor
const cursor = editor.getCursor(); //
const state = view.getState()
const editorView = view.editor.cm as EditorView
// 转化类
EditorView.state() // EditorState from "@codemirror/state"
补充:ts报错说找不到属性的问题
// @ts-ignore 这里会说View没有editor属性 const editor: Editor = view.editor // @ts-ignore 这里会说Editor没有cm属性 const editorView: EditorView = editor.cm
this.app.workspace.getActiveViewOfType(MarkdownView);
中,this和plugin_this都可以
他们的.app出来的内容是一样,区别在于类型声明不同。
所以看起来用plugin_this会报错而this不会,但其实本质是一样的,报错的话加个@ts-ignore就行了。View类型看原型没有,但打印出来它确实是有editor参数的
Ediot类型看原型没有,但打印出来它确实是有cm参数的
类型验证
const view: View|null = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) return false
// @ts-ignore 这里会说View没有editor属性
const editor: Editor = view.editor
// @ts-ignore 这里会说Editor没有cm属性
const editorView: EditorView = editor.cm
const editorState: EditorState = editorView.state
// const state: any = view.getState() // 这个不是
const cursor: EditorPosition = editor.getCursor();
// console.log("【Msg】 this", this.constructor.name, this instanceof Window, typeof(this), this)
console.log("【Msg】 cursor", cursor.discriminator === "EditorPosition", cursor) // False Object
console.log("【Msg】 view", view instanceof View, view) // from 'obsidian'
console.log("【Msg】 editor", editor instanceof Editor, editor) // from 'obsidian'
console.log("【Msg】 editorState", editorState instanceof EditorState, editorState) // from '@codemirror/state'
console.log("【Msg】 editorView", editorView instanceof EditorView, editorView) // from "@codemirror/view"
console.log("【Msg】 state", state instanceof EditorState, state) // False Object any
console.log("【Msg】 this", this instanceof Window, this)
console.log("【Msg】 plugin_this", plugin_this instanceof AnyBlockPlugin, plugin_this)