エディタDOM構造の簡素化とコードリファクタリング

basecamp/lexxy

LexxyエディタのDOM構造から不要な<div>ラッピングを削除し、Lexicalの更新ループを最適化する変更が行われました。これにより、エディタの初期化処理が簡潔になり、不要な再レンダリングが削減されます。

背景

Lexxyエディタは、RailsヘルパーとJavaScript APIの両方で、エディタの値を二重の<div>で囲んでいました。Railsヘルパーのlexxy_rich_textarea_tagは値を<div>で囲み、JavaScript側のset value()でも再度<div>で囲んでいたため、実際のコンテンツが不要な階層の中に埋もれていました。

また、editor.update()内で呼び出される関数が、さらに新しい更新ループを作成していたため、パフォーマンスの観点から改善の余地がありました。#724はこれらの問題を解消しています。

技術的な変更

DOM構造の簡素化

Railsヘルパー側の変更:

lib/lexxy/rich_text_area_tag.rbで、値を<div>で囲む処理が削除されました。

value = render_custom_attachments_in(value)
-value = "<div>#{value}</div>" if value

options[:name] ||= name
options[:value] ||= value

JavaScript側の変更:

src/elements/editor.js#parseHtmlIntoLexicalNodesメソッドで、HTMLパース時の<div>ラッピングが削除されました。

#parseHtmlIntoLexicalNodes(html) {
  if (!html) html = "<p></p>"
-  const nodes = $generateNodesFromDOM(this.editor, parseHtml(`<div>${html}</div>`))
+  const nodes = $generateNodesFromDOM(this.editor, parseHtml(`${html}`))

この変更に伴い、生のテキストノードを直接処理できるよう、新たに#wrapTextNodeメソッドと#unwrapDecoratorNodeメソッドが追加されました。

// Raw string values produce TextNodes which cannot be appended directly to the RootNode.
// We wrap those in <p>
#wrapTextNode(node) {
  if (!$isTextNode(node)) return node

  const paragraph = $createParagraphNode()
  paragraph.append(node)
  return paragraph
}

// Custom decorator block elements such as action-text-attachments get wrapped into <p> automatically by Lexical.
// We unwrap those.
#unwrapDecoratorNode(node) {
  if ($isParagraphNode(node) && node.getChildrenSize() === 1) {
    const child = node.getFirstChild()
    if ($isDecoratorNode(child) && !child.isInline()) {
      return child
    }
  }
  return node
}

更新ループの最適化

editor.update()内でのみ使用される関数が、新しい更新ループを作成しないようリファクタリングされました。

コマンドディスパッチャーの変更:

src/editor/command_dispatcher.jsdispatchRotateHeadingFormatメソッドから、editor.update()の呼び出しが削除されました。このメソッドは既にeditor.update()のコンテキスト内で呼ばれるため、二重のループが不要です。

dispatchRotateHeadingFormat() {
-  this.editor.update(() => {
-    const selection = $getSelection()
-    if (!$isRangeSelection(selection)) return
+  const selection = $getSelection()
+  if (!$isRangeSelection(selection)) return

-    if ($isRootOrShadowRoot(selection.anchor.getNode())) {
-      selection.insertNodes([ $createHeadingNode("h2") ])
-      return
-    }
+  if ($isRootOrShadowRoot(selection.anchor.getNode())) {
+    selection.insertNodes([ $createHeadingNode("h2") ])
+    return
+  }

Contentsクラスの変更:

同様に、src/editor/contents.jsinsertAtCursorメソッドも、editor.update()の呼び出しが削除されました。配列の最後の要素へのアクセスはat(-1)メソッドに変更されています。

insertAtCursor(node) {
-  this.editor.update(() => {
-    const selection = $getSelection()
-    const selectedNodes = selection?.getNodes()
+  const selection = $getSelection()
+  const selectedNodes = selection?.getNodes()

-    if ($isRangeSelection(selection)) {
-      $insertNodes([ node ])
-    } else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
-      const lastNode = selectedNodes.at(-1)
-      lastNode.insertAfter(node)
-    } else {
-      const root = $getRoot()
-      root.append(node)
-    }
-  })
+  if ($isRangeSelection(selection)) {
+    $insertNodes([ node ])
+  } else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
+    const lastNode = selectedNodes.at(-1)
+    lastNode.insertAfter(node)
+  } else {
+    const root = $getRoot()
+    root.append(node)
+  }
}

その他の改善

WrappedTableNodeの警告抑制:

src/nodes/wrapped_table_node.jsで、$config()importDOM()メソッドが追加されました。

$config() {
  return this.config("wrapped_table_node", { extends: TableNode })
}

static importDOM() {
  return super.importDOM()
}

src/extensions/tables_lexical_extension.jsでは、ノード置換設定にwithKlassプロパティが追加されました。

WrappedTableNode,
{
  replace: TableNode,
-  with: () => new WrappedTableNode()
+  with: () => new WrappedTableNode(),
+  withKlass: WrappedTableNode
},

Stimulusデバッグの無効化:

開発環境でのコンソール出力を削減するため、test/dummy/app/javascript/controllers/application.jsでStimulusのデバッグモードがオフになりました。

-application.debug = true
+application.debug = false

設計判断

値のラッピングを両レイヤーで削除する方針が採用されました。RailsヘルパーとJavaScript APIの両方で<div>ラッピングを削除することで、エディタのDOM構造がシンプルになり、デバッグやスタイリングが容易になります。

既存の更新ループ内で実行される関数については、新しいeditor.update()を呼び出さないよう統一されました。これにより、不要な再レンダリングが削減され、パフォーマンスが向上します。関数が既に更新コンテキスト内で呼ばれることを前提とした設計に変更されています。

テストコードも、新しいDOM構造に合わせて更新されています。test/helpers/lexxy/tag_helper_test.rbでは、値が直接<p>Sample Content</p>として扱われるようになり、余分な<div>のアサーションが削除されました。

これらの変更は、エディタの内部構造を簡素化しつつ、外部APIの互換性を維持した慎重な設計といえます。

まとめ

本PRは、LexxyエディタのDOM構造とレンダリングパイプラインを最適化する変更です。不要な<div>ラッピングの削除と更新ループの効率化により、エディタの初期化が高速化され、コードの可読性も向上しています。WrappedTableNodeのコンソール警告も抑制され、より静かな開発環境が実現されました。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術詳細(各論)→まとめ(結論)」という構成が明確に守られています。特に「設計判断」セクションが設けられており、変更の意図を深く解説することで記事の価値を高めています。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

DOM構造、Lexicalの更新ループ、Stimulusといったトピックは専門知識を持つエンジニアを対象としており、冗長な説明がなく、技術レベルが適切です。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。これにより、非常に読みやすく、理解しやすい構造になっています。

Diff内容との照合 ✓ PASS

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

記事内で引用されているすべてのコードブロックは、提供されたDiff情報と正確に一致しています。変更点(削除・追加)が的確にハイライトされており、読者の理解を助けます。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「DOM構造」「更新ループ」「DecoratorNode」「Railsヘルパー」など、使用されている技術用語はPRの文脈において正確かつ適切です。

説明の技術的正確性 ✓ PASS

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

「二重のdivラッピング」や「更新ループのネスト」といった問題点の説明は、DiffやPR Descriptionによって完全に裏付けられており、技術的に正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張(DOM構造の簡素化、更新ループの最適化、警告の抑制など)は、PRのタイトル、説明、Diffの内容に完全に基づいています。ハルシネーション(創作された情報)は見られません。

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

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

PR番号(#724)や、コード引用におけるファイルパスがすべて正確に記載されています。

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

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

記事のタイトル「エディタDOM構造の簡素化とコードリファクタリング」は、PRの主題である「Remove wrapping <div>, clean up & refactor」を的確に要約しており、内容との整合性が取れています。

外部知識の正確性 ✓ PASS

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

記事の内容はすべて提供されたPR情報に基づいており、バージョンサポート状況やリリース日程といったPR外の知識を持ち出してはいません。

時間表現の正確性 ✓ PASS

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

記事内には「近い将来」や「まもなく」といった曖昧な時間表現はなく、完了した変更として正確に記述されています。