ブロッククオート先頭での改行時にテキストが書式化されないように修正
ブロッククオートの最初の段落先頭で Enter を押すと、直前に空段落が挿入されてクオート内部に残り、続く入力が引用形式になる問題を解消し、カーソルが段落先頭にある場合はクオート外に平文段落を作成するように変更しました。
背景
ブロッククオートの先頭で改行した際に書式が漏れる という挙動は、ユーザーが引用文の上部に新規テキストを入力したときに予期せぬ引用が付加されるという UX の欠陥でした。既存実装では INSERT_PARAGRAPH_COMMAND が空段落をブロッククオート内部に生成し、$escapeFromBlankBlockquoteParagraph がその後処理を担当していましたが、カーソルが最初の非空段落のオフセット0にあるケースを網羅できていませんでした。この問題は Basecamp の内部カード #9985732711 で報告されており、Trix の BC4 挙動(引用上部のテキストは書式化しない)と一致させることが求められました。
技術的な変更
src/extensions/format_escape_extension.js の $escapeFromBlockquote が新たに $escapeBeforeBlockquoteStart と $escapeFromBlankBlockquoteParagraph の両方を呼び出すように変更され、前者がカーソルがブロッククオート最初の段落先頭にあるかを判定して平文段落を前方に挿入します。
function $escapeFromBlockquote() {
return $escapeBeforeBlockquoteStart() || $escapeFromBlankBlockquoteParagraph()
}
$escapeBeforeBlockquoteStart は次のロジックを実装しています。
function $escapeBeforeBlockquoteStart() {
const selection = $getSelection()
if (!$isRangeSelection(selection) || !selection.isCollapsed() || selection.anchor.offset !== 0) return false
const paragraph = $getNearestNodeOfType(selection.anchor.getNode(), ParagraphNode)
if (paragraph && !$isBlankNode(paragraph) && !paragraph.getPreviousSibling()) {
const blockquote = paragraph.getParent()
if ($isQuoteNode(blockquote)) {
blockquote.insertBefore($createParagraphNode())
return true
}
}
return false
}
この実装により、カーソルが最初の非空段落の先頭であり、かつ前に兄弟要素が無い場合に ブロッククオートの直前に新しい空段落 が作成され、ユーザーの入力は引用外の平文として扱われます。既存の $escapeFromBlankBlockquoteParagraph は空段落の分割・移動ロジックを保持し、改行後に空段落が残るケースを引き続き処理します。
設計判断
ハンドラを二段階に分割する設計 が採用されました。元の単一関数はロジックが混在し、条件分岐が複雑化していました。新しい $escapeBeforeBlockquoteStart を導入し、「ブロッククオート開始位置でのエスケープ」 と 「空段落からのエスケープ」 を明確に分離することで、テスト容易性と可読性が向上しました。さらに、$escapeFromBlockquote が短絡評価でいずれかが真なら処理完了とすることで、不要な分岐チェックを回避し、実行コストも僅かに削減されています。
まとめ
ブロッククオート先頭での Enter に対し、引用外に平文段落を挿入するロジックを追加し、書式が意図せず継承される問題を根本的に解消しました。テストスイート(Playwright と Vitest)がすべてパスし、既存機能への影響もなく、ユーザー体験が Trix の期待動作と一致するようになりました。