ソフト改行を含むテキストへのリスト適用を行単位で正確に処理

basecamp/lexxy

Shift+Enterで挿入されるソフト改行(<br>)を含む段落に対してリスト書式を適用すると、段落全体が単一のリストアイテムになってしまうバグが修正されました。この修正により、選択した行のみをリストアイテムとして変換する正確な動作が実現しています。

背景

Lexxyエディタでは、ブロッククォート書式の適用時に同様の問題が既に修正されており、そのための splitParagraphsAtLineBreaks メソッドが Contents クラスに存在していました。しかしリスト書式の適用においては、この前処理が行われていませんでした。

具体的には、<p>First line<br>Second line<br>Third line</p> のようにソフト改行で区切られたテキストで「Second line」を選択してリスト書式を適用すると、期待される <p>First line</p><ul><li>Second line</li></ul><p>Third line</p> ではなく、段落全体が単一リストアイテムとして扱われていました。ブロッククォートの既存修正と同じアプローチをリスト書式にも適用することで、この問題を解消しています。

技術的な変更

変更の核心は、リスト書式のディスパッチ処理を CommandDispatcher から Contents クラスへ移動し、その前処理として段落分割を挿入した点です。

command_dispatcher.js の変更前:

// バレットリスト
this.editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)

// 番号付きリスト
this.editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)

command_dispatcher.js の変更後:

// バレットリスト
this.contents.applyUnorderedListFormat()

// 番号付きリスト
this.contents.applyOrderedListFormat()

INSERT_ORDERED_LIST_COMMANDINSERT_UNORDERED_LIST_COMMAND のインポートも command_dispatcher.js から contents.js へ移動しています。

Contents クラスには以下のメソッドが追加されました:

applyUnorderedListFormat() {
  this.#splitParagraphsAtLineBreaksUnlessInsideList()
  this.editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
}

applyOrderedListFormat() {
  this.#splitParagraphsAtLineBreaksUnlessInsideList()
  this.editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)
}

#splitParagraphsAtLineBreaksUnlessInsideList() {
  if (this.selection.isInsideList) return

  const selection = $getSelection()
  if (!$isRangeSelection(selection)) return

  this.#splitParagraphsAtLineBreaks(selection)
}

さらに、既存の #splitParagraphsAtLineBreaks メソッド内部でも改善が行われています。選択範囲のアンカー・フォーカスノードの参照方法が、ノードキー(getKey())からトップレベル要素(getTopLevelElement())の取得に変更されました。これにより、ネストされたノード構造においても選択範囲の境界を正確に判定できます。

設計判断

リスト内での動作を保護するガード節の設計が重要なポイントです。#splitParagraphsAtLineBreaksUnlessInsideListthis.selection.isInsideListtrue の場合に即座に処理を終了します。これにより、リストアイテム内でShift+Enterを押した際に行われる改行(<br>)の挿入という既存の動作が壊れないようになっています。リスト書式の「適用」操作と「内部での編集」操作を、同一コードパスを通じながら条件分岐で適切に振り分ける設計です。

責務の集約という観点も見て取れます。変更前はリストコマンドのディスパッチが CommandDispatcher の責務でしたが、「段落分割してからリストを適用する」という複合操作を Contents クラスに集約することで、コマンドのディスパッチに関する知識(インポートを含む)も Contents 側に一元化されています。ブロッククォートと同じパターンに揃えることで、書式適用ロジックの一貫性も高まっています。

まとめ

本PRは、ブロッククォートで実績のある段落分割アプローチをリスト書式にも適用した変更です。ディスパッチロジックを Contents クラスへ移管するリファクタリングと、リスト内外での動作を正確に分岐するガード節の追加により、Shift+Enterの文脈によって異なる動作を一貫した設計で実現しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
0ddb6a0a

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の構成が明確で、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

Lexxyエディタの内部実装に関する専門的な内容であり、過度な説明がなく、対象読者であるエンジニアに適切です。

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

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

各セクション、各パラグラフがトピックセンテンスで始まり、要点が明確です。1段落1トピックの原則も守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードは、`command_dispatcher.js` と `contents.js` の変更点を正確に反映しています。`getTopLevelElement()`への変更点に関する言及も的確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Lexicalフレームワークの専門用語(`dispatchCommand`, `getTopLevelElement`など)や、一般的な設計用語(ガード節、責務の集約)が正確かつ効果的に使用されています。

説明の技術的正確性 ✓ PASS

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

コマンドのディスパッチ処理の移管や、リスト内での動作を保護するガード節の役割など、技術的な変更点に関する説明が論理的かつ正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiff内のコードによって裏付けられています。「設計判断」セクションはコードの意図を深く分析したものであり、ハルシネーションではありません。

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

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

PR番号(#946)が正確に記載され、正しくリンクされています。

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

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

記事のタイトルはPRの主題「ソフト改行を段落に分割してからリストを挿入する」を、ユーザーへの影響がわかる形で要約しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

記事の内容は提供されたPR情報に完全に準拠しており、バージョン情報やリリース日程など、PRに記載のない外部知識の追加はありません。

時間表現の正確性 ✓ PASS

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

「既に修正されており」といった時間表現は、PR内の「existing fix」という記述と整合性が取れており、正確です。