Ordered List の初期値での番号付与バグ修正

basecamp/lexxy

Lexxy で <ol> をエディタの初期 value としてロードした際、リスト項目がすべて value="1" になる問題を、内部フォーム値の取得タイミングとエディタ更新のフローを調整することで解消した。

背景

バグの現象: 初期 value に含まれる順序付きリストは、エディタが生成した HTML が <li value="1"> を全項目に付与し、連番が失われる。

根本原因: #initialEditorState コールバック内で editor.update() が走り、その中で editor.read() が呼ばれると、Lexical は半完了の更新をコミットし、ノード変換(特に ListNodeupdateChildrenListItemValue)をスキップする。その結果、<li>value 属性はデフォルト 1 のままになる。

影響範囲: 編集操作で生成したリストは正しく番号付与されるが、最初から HTML を value 属性で渡すケースだけが対象となり、ユーザーがエディタを初期化するときに不整合が顕在化する。

技術的な変更

内部フォーム値取得のタイミング変更: $initialEditorState のコールバックから #setInternalFormValue(this.#readSanitizedEditorValue(editor)) を除去し、エディタ構築直後に #setInternalFormValue(this.value, { suppressEvent: true }) を実行するようにした。これにより editor.read() が完了した更新上で走り、すべてのリスト変換が適用された状態で内部値がシードされる。

@@ -409,7 +411,6 @@
       $initialEditorState: (editor) => {
         this.#configureSanitizer(editor)
         this.#loadInitialValue(editor)
-        this.#setInternalFormValue(this.#readSanitizedEditorValue(editor))
       },

#readSanitizedEditorValue のシグネチャ簡素化: 引数 editor を削除し、常に this.editor を使用するようにした。呼び出し側が editor を渡す必要がなくなることで、コードパスが一本化された。

@@ -338,8 +338,8 @@
-  #readSanitizedEditorValue(editor = this.editor) {
-    return editor?.read(() => {
+  #readSanitizedEditorValue() {
+    return this.editor?.read(() => {
       return sanitize($generateHtmlFromNodes(this.editor, null))
     }) ?? null
   }

#setInternalFormValue にオプション導入: 新たに { suppressEvent = false } を受け取り、変更検知ロジックを html !== this.#previousInternalFormValue に簡素化した。初期シード時に suppressEvent: true を渡すことで、不要な lexxy:change イベントの発火を防止しつつ内部値は更新される。

@@ -477,13 +478,13 @@
-  #setInternalFormValue(html) {
-    const changed = this.#previousInternalFormValue !== null && html !== this.#previousInternalFormValue
+  #setInternalFormValue(html, { suppressEvent = false } = {}) {
+    const changed = html !== this.#previousInternalFormValue

     this.internals.setFormValue(html)
     this.#previousInternalFormValue = html

-    if (changed) {
+    if (changed) {

#initialize の呼び出し順序調整: 旧来は最初に #synchronizeWithChanges() が走っていたが、内部フォーム値設定を先に行い、続いて #synchronizeWithChanges() を呼び出すよう変更した。これにより、最初の変更リスナーが登録される前に正しいシードが確定する。

@@ -371,7 +371,9 @@
-    this.#resetBeforeTurboCaches()
+    this.#resetBeforeTurboCaches()
+
+    this.#setInternalFormValue(this.value, { suppressEvent: true })
+    this.#synchronizeWithChanges()

テストの追加: ordered_list_initial_value.test.js と対応する HTML フィクスチャを新規作成し、初期値として渡した <ol>value="1", value="2", value="3" と連番になることを確認した。フル Playwright と Rails テストスイートともに全ケースがパスし、回帰防止が保証された。

設計判断

スパースなイベント抑制: 初期シード時に lexxy:change を発火させない設計は、09c4b609 で導入された「spurious change 防止」ロジックと整合性がある。#setInternalFormValuesuppressEvent オプションを持たせることで、既存コードはデフォルトでイベントを発行しつつ、必要な箇所だけ抑制できる。

後方互換性の確保: メソッドシグネチャの変更は引数が省略可能になるだけで、外部から #setInternalFormValue を直接呼んでいるケースは少なく、#readSanitizedEditorValue も内部のみ使用されるため、公開 API への影響は最小。既存のエディタ初期化フローは変わらず動作し、変更は内部実装のタイミング最適化に留まる。

最小侵襲の実装: 変更箇所は 3 つのメソッドと呼び出し順序に限定され、ロジック自体は追加の条件分岐やオプション処理だけで実装コストが低い。これにより、バグ修正とコードベースの安定性が同時に確保された。

まとめ

初期 value に含まれる順序付きリストが正しく番号付与されなかった根本原因は、エディタ更新中の editor.read() が変換をスキップしたことにあった。内部フォーム値の取得を更新完了後に行い、#setInternalFormValue に抑制オプションを導入したことで、変換が正しく適用されつつ不要なイベントを防げた。テスト追加により回帰が防止され、既存挙動との互換性も維持された。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
cc9191c2

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的変更、設計判断、まとめの全てが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しいdiffシンタックスですが、PRリンクが `[PR #1103](URL)` となっており、要求される `[#1103](URL)` 形式と異なります。

対象読者への適合性 ✓ PASS

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

専門的な用語と実装詳細で構成されており、エンジニア向けとして適切です。

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

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

各セクションは総論・各論・結論の構成が保たれ、段落はトピックセンテンスで始まり、長さも適切です。

Diff内容との照合 ✓ PASS

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

記事中のdiffブロックは提供されたDiffと一致しており、削除・追加内容に相違はありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用された技術用語はPRおよびコードと整合しており、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

根本原因と修正内容の説明はPRの記述と一致し、技術的に正確です。

事実の突合 ✓ PASS

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

記事の全ての主張はPRタイトル、説明、Diff、テスト追加情報で裏付けられています。

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

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

PR番号やファイル名、行番号などの数値は正しく記載されています。

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

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

記事タイトルはPRの内容を日本語で適切に要約しており、一致しています。

外部知識の正確性 ✓ PASS

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

バージョンやリリーススケジュール等、PRに含まれない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PRの記述と整合しています。