插件案例剖析
插件案例剖析
常用工具手册
Plugin类
import { Plugin } from "obsidian";
export default class ExamplePlugin extends Plugin {
async onload() {
// highlight-next-line
console.log('loading plugin')
}
async onunload() {
// highlight-next-line
console.log('unloading plugin')
}
}
CodeMirror5
与编辑器扩展通信
import { EditorView } from "@codemirror/view";
// @ts-expect-error, not typed
const editorView = view.editor.cm as EditorView;
this.addCommand({
id: "example-editor-command",
name: "Example editor command",
editorCallback: (editor, view) => {
// @ts-expect-error, not typed
const editorView = view.editor.cm as EditorView;
const plugin = editorView.plugin(examplePlugin);
if (plugin) {
plugin.addPointerToSelection(editorView);
}
},
});
您可以编辑器视图内直接派发变动以及state effects。
this.addCommand({
id: "example-editor-command",
name: "Example editor command",
editorCallback: (editor, view) => {
// @ts-expect-error, not typed
const editorView = view.editor.cm as EditorView;
editorView.dispatch({
effects: [
// ...
]
});
},
});
小部件
小部件是您添加到编辑器中的自定义 HTML 标签。您可以在文档中的特定位置插入一个小部件,或者用一个小部件替换一段内容。
下例中定义了一个返回 <span>👉</span>
HTML 标签的小部件。您将在稍后使用到它。
import { EditorView, WidgetType } from "@codemirror/view";
export class EmojiWidget extends WidgetType {
toDOM(view: EditorView): HTMLElement {
const div = document.createElement("span");
div.innerText = "👉";
return div;
}
}
要想使用 emoji 小部件替换您文档中的一部分内容,可以使用替换装饰。
const decoration = Decoration.replace({
widget: new EmojiWidget()
});
纯Obsidian接口
List-Callout
比较简单
// 超简化版代码
export default class ListCalloutsPlugin extends Plugin {
async onload() {
await this.loadSettings();
this.buildPostProcessorConfig(); // 后处理器的配置
this.addSettingTab(); // 设置
this.registerMarkdownPostProcessor(); // md后处理器(渲染模式用)
this.registerEditorExtension(); // 编辑器扩展(实时编辑时用)
}
}
Ad
import type codemirror from "codemirror";
// 超简化版代码
export default class ObsidianAdmonition extends Plugin {
this.app.workspace.onLayoutReady(async () => {
this.addChild
this.registerEditorSuggest(); // 编辑建议
Object.keys(this.admonitions).forEach((type) => {
this.registerType(type);
});
this.addSettingTab(); // 设置
this.addCommand({name: "Collapse Admonitions in Note"}); // 加命令
this.addCommand({name: "Open Admonitions in Note"});
this.addCommand({name: "Insert Admonition"});
this.addCommand({name: "Insert Callout"});
});
registerType(){
MarkdownPreviewRenderer.unregisterCodeBlockPostProcessor()
this.registerMarkdownCodeBlockProcessor(); // 代码块处理
this.registerCommandsFor(admonition); // 这个不是原方法,自己定义的
}
}
捋一下调用关系
/** 【捋一下调用关系】
* 主入口onload() 调用
* registerType > this.registerMarkdownCodeBlockProcessor(注册代码块事件) 调用
* postprocessor(此函数,后处理器)调用
* getAdmonitionElement(获取Ad元素)
* renderAdmonitionContent(渲染Ad内容)调用
* getAdmonitionContentElement(获取Ad内容的元素)
*/
Table-Extended
(详见注释版源代码)
用了Markdowm-it工具
// 超简化版代码
export default class TableExtended extends Plugin {
async onload(): Promise<void> {
this.addSettingTab(new TableExtendedSettingTab(this.app, this)); // 设置
MarkdownPreviewRenderer.registerPostProcessor(this.processNativeTable); // ???后处理器
this.registerMarkdownCodeBlockProcessor("tx", this.renderFromMD); // 代码块处理
this.registerMarkdownPostProcessor(this.processTextSection); // md后处理器(渲染模式用)
}
}
后处理器:
processTextSection = (el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
// el contains only els for one block in preview;
// el contains els for all blocks in export2pdf
for (const child of el.children) {
let p: HTMLParagraphElement;
if (child instanceof HTMLParagraphElement) {
p = child;
} else if (
child instanceof HTMLQuoteElement &&
child.firstElementChild instanceof HTMLParagraphElement
) {
p = child.firstElementChild;
} else continue;
let result;
if (p.innerHTML.startsWith("-tx-")) {
const src = getSourceMarkdown(el, ctx);
if (!src) {
console.warn("failed to get Markdown text, escaping...");
} else if ((result = src.match(prefixPatternInMD))) {
const footnoteSelector = "sup.footnote-ref";
// save footnote refs
const footnoteRefs = [
...el.querySelectorAll(footnoteSelector),
] as HTMLElement[];
// footnote refs is replaced by new ones during rendering
this.renderFromMD(src.substring(result[0].length), el, ctx);
// post process to revert footnote refs
for (const newRefs of el.querySelectorAll(footnoteSelector)) {
newRefs.replaceWith(footnoteRefs.shift()!);
}
for (const fnSection of el.querySelectorAll("section.footnotes")) {
fnSection.remove();
}
}
}
}
};
使用了CM接口
Decoration类
cm6-Decoration
corateView(view: EditorView): DecorationSet {
/* 注释掉这个实际上装饰将不会触发/更新,除非视口改变 */
// 只有在初始加载时,才会同时在所有窗格/叶/视图/注释上运行
if (!view.hasFocus) {
console.log("This Editor does not have focus so skip...", view, view.hasFocus);
this.myPlugin.cm6LastEditorFocus = { file: this.getTFileFromView(view), focused: false };
return Decoration.none;
}
/* 现在不需要这段代码,但是保存起来可以方便地获得活动的cm编辑器 */
const cmEditor = this.getCmEditorFromView(view);
if (cmEditor) {
const getCursor = cmEditor.getCursor();
const getLine = cmEditor.getLine(getCursor.line);
// console.log("getCursor:", getCursor);
// console.log("getLine:", getLine);
}
}
获取一些东西 (TFile和Editor)
import { editorInfoField } from 'obsidian'; // 我刚开始还以为是个变量,原来是个StateField<MarkdownFileInfo>类型的常量
getTFileFromView(view: EditorView): TFile {
// 从当前的EditorView获取TFile
const myTFile = view.state.field(editorInfoField).file;
return myTFile;
}
getCmEditorFromView(view: EditorView): Editor | null {
// 获取CM编辑器对象,用于抓取光标、行等
const cmEditor = view.state.field(editorInfoField).editor;
if (cmEditor) {
return cmEditor;
} else {
return null;
}
}
核心
// Plugin
export default class MyPlugin extends Plugin {
editorExtension: Extension[] = [];
async onload() {
this.registerEditorExtension(this.editorExtension);
}
// 【启动高亮】
this.editorExtension.push(suggestionsExtension(this));
this.app.workspace.updateOptions();
}
// ViewPlugin
const suggestionsExtension = (plugin: MyPlugin): ViewPlugin<PluginValue> => {
return ViewPlugin.fromClass(
// 这个类有个构造参数 view: EditorView
class {
decorations: DecorationSet;
myPlugin: MyPlugin;
// 对于每个被打开的文档,都会执行一次
constructor(view: EditorView) {}
// 当前焦点文档发生变化时(包括光标位置的改变和上下滚动文档导致的visibleRanges改变)
public update(update: ViewUpdate): void {}
},
{
decorations: (view) => view.decorations,
eventHandlers: {
mousedown: (e: MouseEvent, view: EditorView) => {},
},
}
);
};
常用调试
const editorView = view.editor.cm as EditorView; // 此view非彼view,此view不是EditorView,不知道哪来的
// 或
const view = ViewPlugin.fromClass 中的 view: EditorView
const myFile: TFile = view.state.field(editorInfoField).file;
// string类
view.state.doc.toString()
for (const { from, to } of visibleRanges)
view.hasFocus
view.state.selection
const textToHighlight = view.state.sliceDoc(from, to)
const visibleRange = from-to, textToHighlight
const matchesToIgnore = textToHighlight.split(regExIgnore);
for (const eachPart of matchesToIgnore)
update: ViewUpdate // 您可以在 `update()` 方法中添加 `console.log(update);` 这行代码以在控制台中打印所有的更新内容
const cmEditor = view.state.field(editorInfoField).editor;
const cursor = cmEditor.getCursor();
Decoration 压缩版、精简版(AnyBlockPlugin_beta)
export default class AnyBlockPlugin extends Plugin {
async onload() {
this.registerEditorExtension(this.editorExtension(this));
}
editorExtension(plugin_this: AnyBlockPlugin) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(/*plugin_this*/view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(/*plugin_this*/update.view);
}
}
// @private
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>(); // 范围生成器
let underlineDecoration: Decoration = Decoration.mark({ // 核心,装饰四个主要方法里的mark方法 —— 标记装饰
class: 'cm-suggestion-candidate',
attributes: {
'data-index-keyword': "X",
'data-position-start': `20`,
'data-position-end': `30`,
}
});
builder.add(20, 30, underlineDecoration)
return builder.finish();
}
},
{
decorations: (v) => v.decorations,
},
)
}
}
Obsidian 手册的 ViewPlugin 案例
import { syntaxTree } from "@codemirror/language";
import { RangeSetBuilder } from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
PluginSpec,
PluginValue,
ViewPlugin,
ViewUpdate,
WidgetType,
} from "@codemirror/view";
import { EmojiWidget } from "emoji";
// 基本都是规范写法
class EmojiListPlugin implements PluginValue {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
destroy() {}
// @Private
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
for (let { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter(node) {
if (node.type.name.startsWith("list")) {
// Position of the '-' or the '*'.
const listCharFrom = node.from - 2;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new EmojiWidget(),
})
);
}
},
});
}
return builder.finish();
}
}
const pluginSpec: PluginSpec<EmojiListPlugin> = {
decorations: (value: EmojiListPlugin) => value.decorations,
};
export const emojiListPlugin = ViewPlugin.fromClass(
EmojiListPlugin,
pluginSpec
);
链接到当前文件 0
没有文件链接到当前文件