Lexxy拡張システムを統一的なLexxyExtension基底クラスへ移行

basecamp/lexxy

LexxyのエディタPR #725 は、複数の拡張機能実装を LexxyExtension 基底クラスによる統一的な仕組みへ再編成しました。これにより、拡張機能の有効化制御とライフサイクル管理が明確になり、条件分岐コードを削減しています。

背景

これまで HighlightExtensionTrixContentExtensionTablesExtension はそれぞれ異なる方法で初期化・管理されていました。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.jsbaseExtensions ゲッターでベース拡張機能を定義し、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 時にハンドラが確実に登録解除されるようになりました。HighlightExtensionTablesExtensionregister メソッドが mergeRegister でラップされた戻り値を返すように変更されています。

設計判断

拡張機能自身が有効化条件を定義する設計が採用されました。

これまでは editor.js が拡張機能の有効化を判断していましたが、enabled ゲッターを各拡張機能に実装することで、その責務を拡張機能側に移譲しています。HighlightExtensionTrixContentExtensionTablesExtension はいずれも supportsRichText を確認し、プレーンテキストモードでは自動的に無効化される実装になりました。

この判断により、新しい拡張機能を追加する際に editor.js を変更する必要がなくなり、拡張性が向上しています。各拡張機能がカプセル化され、その振る舞いを自己完結的に定義できるようになりました。

基底クラスのメソッドをオプショナルにする設計も採用されました。enabled はデフォルトで true を返し、lexicalExtensionnull を返すため、最小限の実装で拡張機能を作成できます。initializeToolbar も空実装が提供されており、必要な場合のみオーバーライドする形です。

これにより、拡張機能の複雑さに応じて実装範囲を調整でき、シンプルな拡張機能から高度な拡張機能までを同じインターフェースで扱えるようになっています。

まとめ

本PRは、複数の実装パターンで管理されていた拡張機能を LexxyExtension 基底クラスによる統一的な仕組みに移行しました。拡張機能が自身の有効化条件を定義することで、エディタ本体から条件分岐を削除し、mergeRegister によるクリーンアップ処理を導入することで、拡張機能のライフサイクル管理も改善されています。これらの変更により、Lexxyの拡張機能アーキテクチャがより保守性と拡張性の高いものになりました。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
3回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の構成が明確に適用されており、すべての必須要素が含まれています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)とPR番号のリンク記法([#725](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

LexicalフレームワークやJavaScriptのクラス継承に関する知識を前提としており、専門知識を持つエンジニアという対象読者に適した技術レベルと表現で書かれています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のすべてのコードブロックは、提供されたDiff情報と一致しています。ファイルパスの指定も正確であり、変更前後のコード比較が的確に行われています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「基底クラス」「ゲッター」「ライフサイクル管理」「mergeRegister」など、PRの内容と文脈に沿った正確な技術用語が使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

拡張機能の統一、条件分岐の削除、クリーンアップ処理の導入といった技術的な説明は、すべてDiffやPR Descriptionによって裏付けられており、論理的かつ正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのタイトル、Description、またはDiff内のコード変更に直接基づいています。根拠のない推測や憶測(ハルシネーション)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#725)や、コード内のクラス名・メソッド名などの固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「Lexxy拡張システムを統一的なLexxyExtension基底クラスへ移行」は、PRの主題である「Organize extensions」の具体的な内容を的確に表現しており、内容と一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事には、バージョン情報やリリース予定など、PRで言及されていない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「これまで」「変更前」「変更後」といった時間表現は、PRの文脈内で正確に使用されており、時間的な歪曲はありません。