Lexxy拡張システムを統一的なLexxyExtension基底クラスへ移行
LexxyのエディタPR #725 は、複数の拡張機能実装を LexxyExtension 基底クラスによる統一的な仕組みへ再編成しました。これにより、拡張機能の有効化制御とライフサイクル管理が明確になり、条件分岐コードを削減しています。
背景
これまで HighlightExtension、TrixContentExtension、TablesExtension はそれぞれ異なる方法で初期化・管理されていました。editor.highlighter という専用プロパティが存在したり、editor.js 内で条件分岐により拡張を読み込んだりしており、一貫性のないインターフェースが拡張機能の追加を複雑にしていました。
PRではこの状態を「various mechanisms」と表現し、統一的な LexxyExtension 基底クラスの導入によってこの問題を解消しています。拡張機能が自身の有効化条件を定義することで、editor.js から条件分岐コードを削除できるようになりました。
技術的な変更
LexxyExtension基底クラスの追加により、すべての拡張機能が共通インターフェースを持つようになりました。このクラスは app/assets/javascript/lexxy.js に追加されました。
追加された基底クラス:
class LexxyExtension {
#editorElement
constructor(editorElement) {
this.#editorElement = editorElement;
}
get editorElement() {
return this.#editorElement
}
get editorConfig() {
return this.#editorElement.config
}
// optional: defaults to true
get enabled() {
return true
}
get lexicalExtension() {
return null
}
initializeToolbar(_lexxyToolbar) {
}
}
HighlightExtensionの再構成では、独立した Highlighter クラスが削除され、LexxyExtension を継承する形に変更されました。enabled ゲッターで supportsRichText を返すことで、プレーンテキストモードでは自動的に無効化されます。
変更前(highlighter.js):
export default class Highlighter {
constructor(editorElement) {
this.editorElement = editorElement
}
toggle(styles) {
this.editor.dispatchCommand(TOGGLE_HIGHLIGHT_COMMAND, styles)
}
remove() {
this.toggle({ "color": null, "background-color": null })
}
}
変更後(highlight_extension.js):
export class HighlightExtension extends LexxyExtension {
get enabled() {
return this.editorElement.supportsRichText
}
get lexicalExtension() {
const extension = defineExtension({
dependencies: [ RichTextExtension ],
name: "lexxy/highlight",
register(editor, config) {
const canonicalizers = buildCanonicalizers(config)
return mergeRegister(
editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, $toggleSelectionStyles, COMMAND_PRIORITY_NORMAL),
editor.registerNodeTransform(TextNode, $syncHighlightWithStyle),
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers))
)
}
})
return extension
}
}
コマンドディスパッチの変更では、editor.highlighter への直接アクセスが Lexical のコマンドシステムを使った統一的な方法に変更されました。
変更前(command_dispatcher.js):
dispatchToggleHighlight(styles) {
this.highlighter.toggle(styles)
}
dispatchRemoveHighlight() {
this.highlighter.remove()
}
変更後(command_dispatcher.js):
dispatchToggleHighlight(styles) {
this.editor.dispatchCommand(TOGGLE_HIGHLIGHT_COMMAND, styles)
}
dispatchRemoveHighlight() {
this.editor.dispatchCommand(REMOVE_HIGHLIGHT_COMMAND)
}
拡張機能の登録方法も変更され、editor.js の baseExtensions ゲッターでベース拡張機能を定義し、extensions.js で外部設定とマージする形になりました。
変更後(editor.js):
get baseExtensions() {
return [
HighlightExtension,
TrixContentExtension,
TablesExtension
]
}
変更後(extensions.js):
get #baseExtensions() {
return this.lexxyElement.baseExtensions
}
get #configuredExtensions() {
return Lexxy.global.get("extensions")
}
#initializeExtensions() {
const extensionDefinitions = this.#baseExtensions.concat(this.#configuredExtensions)
return extensionDefinitions.map(
extension => new extension(this.lexxyElement)
)
}
mergeRegister によるクリーンアップ処理も追加され、エディタの dispose 時にハンドラが確実に登録解除されるようになりました。HighlightExtension と TablesExtension の register メソッドが mergeRegister でラップされた戻り値を返すように変更されています。
設計判断
拡張機能自身が有効化条件を定義する設計が採用されました。
これまでは editor.js が拡張機能の有効化を判断していましたが、enabled ゲッターを各拡張機能に実装することで、その責務を拡張機能側に移譲しています。HighlightExtension と TrixContentExtension、TablesExtension はいずれも supportsRichText を確認し、プレーンテキストモードでは自動的に無効化される実装になりました。
この判断により、新しい拡張機能を追加する際に editor.js を変更する必要がなくなり、拡張性が向上しています。各拡張機能がカプセル化され、その振る舞いを自己完結的に定義できるようになりました。
基底クラスのメソッドをオプショナルにする設計も採用されました。enabled はデフォルトで true を返し、lexicalExtension は null を返すため、最小限の実装で拡張機能を作成できます。initializeToolbar も空実装が提供されており、必要な場合のみオーバーライドする形です。
これにより、拡張機能の複雑さに応じて実装範囲を調整でき、シンプルな拡張機能から高度な拡張機能までを同じインターフェースで扱えるようになっています。
まとめ
本PRは、複数の実装パターンで管理されていた拡張機能を LexxyExtension 基底クラスによる統一的な仕組みに移行しました。拡張機能が自身の有効化条件を定義することで、エディタ本体から条件分岐を削除し、mergeRegister によるクリーンアップ処理を導入することで、拡張機能のライフサイクル管理も改善されています。これらの変更により、Lexxyの拡張機能アーキテクチャがより保守性と拡張性の高いものになりました。