Obsidianのプラグイン開発で独自のコードブロックを定義する

Obsidianのプラグイン開発で独自のコードブロックを定義すれば、ノートにいろんな情報を埋め込んで表示できます。
たとえばDataviewであれば絞り込んだノートの一覧、Tasksであればタスクの一覧などです。

ObsidianのAPIに関する公式ドキュメントを読んでも、APIの実際の使い方までは書いていません。
本記事では、APIの実装例を挙げながら「独自のコードブロックを定義して使えるようにする方法」を紹介します。

対象はいうまでもなくのObsidianのプラグイン開発者です。

独自のコードブロックの定義方法

まずは一番単純な例から紹介します。
シンプルに、受け取った内容に対してHello, 〇〇を表示させてみます。

main.ts
import { Plugin } from "obsidian";
export default class CreateCodeblock extends Plugin {
async onload() {
this.registerMarkdownCodeBlockProcessor(
"sample",
async (source, element, context) => {
const container = element.createEl("div");
container.innerText = `Hello, ${source}!`;
},
);
}
}

プラグインのonloadの中でregisterMarkdownCodeBlockProcessorを使うことでコードブロックを定義できます。
次のセクションで各引数を詳しくみていきましょう。

第1引数に言語名

第1引数がコードブロックの言語名です。本記事の例では言語名をsampleとします。よって、コードブロックは次のように書きます。

```sample
Ninja
```

第2引数にハンドラ

第2引数には「コードブロックの中身を受け取って表示させるハンドラ」を渡します。

例で出てきたハンドラの中身を見てみましょう。

async (source, element, context) => {
const container = element.createEl("div");
container.innerText = `Hello, ${source}!`;
},

sourceはコードブロックの中に書いた文字列です。前述の例ではNinjaが代入されます。

elementの中で要素をこねくり回すことで、自由に表示できます。

contextは次のセクションで説明します。

公式ドキュメント:registerMarkdownCodeBlockProcessor - Developer Documentation

コンポーネントのレンダリング

Reactのコンポーネントの表示方法は2パターンあります。

elementを使うパターン

まずはregisterMarkdownCodeBlockProcessorに渡すハンドラの引数「element」に対してcreateRootを使うパターンです。

main.ts
this.registerMarkdownCodeBlockProcessor(
this._language,
async (source, element, context) => {
myRender(element, source);
},
);
myRender.tsx
import "./app.css";
import { createRoot } from "react-dom/client";
export function myRender(containerEl: Element, source: string) {
const root = createRoot(containerEl);
root.render(<p>Hello, {source}!!!</p>);
}

createRootの呼び出しはtsxファイルとして切り分けています。
最初からmain.tsの方をmain.tsxにして、main.tsxの中にコンポーネントを書くのもいいかもしれませんね。

基本はこっちのパターンで十分だと思います。

contextを使うパターン

続いて、ハンドラの引数contextを使う例です。こちらはライフサイクルを含めたコンポーネントを作りたい時に使います。

まずはさっくりとした例から。Appなるものをcontext.addChildします。

main.ts
this.registerMarkdownCodeBlockProcessor(
"sample",
async (source, element, context) => {
const renderer = new App(element, source);
context.addChild(renderer);
},
);
App.tsx
import "./app.css";
import { MarkdownRenderChild } from "obsidian";
import { type Root, createRoot } from "react-dom/client";
export class App extends MarkdownRenderChild {
root: Root;
source: string;
constructor(containerEl: HTMLElement, source: string) {
super(containerEl);
this.root = createRoot(containerEl);
this.source = source;
}
async onload() {
this.root.render(<div>Hello, {this.source}(Component)!!!</div>);
}
async onunload() {
this.root.unmount();
}
}

MarkdownRenderChildを継承したクラスでonloadonunloadを定義しています。これがライフサイクルの部分です。

具体的に。このパターンを採用しているobsidian-plugin-abcjsでは、onunloadで再生停止の処理をしているようです。

公式ドキュメント:MarkdownRenderChild - Developer Documentation

コードブロックの中身をJSとして処理させる

コードブロックの中身をJavaScriptとして処理する方法も解説します。

registerMarkdownCodeBlockProcessorに渡すハンドラの引数sourceを次のような関数に渡します。

async function execute(source: string) {
const func = new Function(
`return new Promise((s,r)=>{(async ()=>{
return ${source};
})().then(s).catch(r)})`,
);
return await func();
}

このFunctionFunctionコンストラクタというものです。

戻り値がなければreturnを外すなど、適宜書き換えてください。

コードブロックの中でAPIを呼び出したい

コードブロックの中でDataviewのような他のプラグインのAPIを呼び出せるようにするには、次のように1行足します。

async function execute(source: string) {
const func = new Function(
`return new Promise((s,r)=>{(async ()=>{
const dv = DataviewAPI;
return ${source};
})().then(s).catch(r)})`,
);
return await func();
}

エラーだったら教えてあげたい

コードブロックを扱うということは、入力ミスがありえます。
文法エラーやパースできなかったときに、そのエラー内容を表示してあげると親切です。

次の例がエラーだったらその内容を表示するReactのコンポーネントです。

// ...
type Props = {
rawOptions: unknown;
};
export default function App({ rawOptions }: Props) {
const { options, error } = useCodeBlock(rawOptions); // ここでJS実行やパース
if (error) {
console.log(error);
return <pre>{String(error)}</pre>;
}
return (<MyComponent options={options} />);
}

useCodeBlockの中で「コードブロックの文字列をJavaScriptとして処理」「パース」など行います。
その過程でエラーが出たらその旨を表示するだけです。

シンタックスハイライト

コードブロックはそのままだとただの文字列扱いで、装飾などはされません。

コードブロックでハイライトなしの例

次のようにコードブロックの文字列にシンタックスハイライトをつけることができます。

コードブロックでハイライトした例

ではコードを見てみましょう。JavaScriptとしてシンタックスハイライトが効くように設定しました。

import { Plugin } from "obsidian";
export default class CreateCodeblock extends Plugin {
private _language = "sample";
async onload() {
this.registerMarkdownCodeBlockProcessor(
// ... 省略
);
this.registerCodeBlockHighlithing();
this.register(() => this.unregisterCodeBlockHighlithing());
}
registerCodeBlockHighlithing() {
window.CodeMirror.defineMode(this._language, (config) =>
window.CodeMirror.getMode(config, "javascript"),
);
}
unregisterCodeBlockHighlithing() {
window.CodeMirror.defineMode(this._language, (config) =>
window.CodeMirror.getMode(config, "null"),
);
}
}

onloadの中でシンタックスハイライトの定義と解除の登録を書きます。

シンタックスハイライト自体はObsidianではなくCodeMirrorでやっています。CodeMirror: Language ModesにCodeMirrorのデフォルトで使える言語が書いてあります。

参考

サンプルリポジトリ

本記事で扱ったサンプルコードは次のリポジトリにあります。

eetann/obsidian-create-codeblock


以上、Obsidianのプラグイン開発におけるコードブロックの扱い方でした。

残念ながら開発中のプラグインでは結局使わないことになりましたが、せっかく調べたので備忘録として残したのでした。ちゃんちゃん。