ブロック引用符貼り付け時の段落間空白行を保持
Lexxy の Markdown 貼り付け機能で、ブロック引用符内部の複数段落間の空白行が失われる不具合を解消しました。この変更により、引用内の段落構造が正しく視覚的に分離されます。
背景
Markdown のブロック引用符内で空白行が消える問題は、エディタが貼り付け時に段落間のスペーサーを付与しないことが原因でした。トップレベルの段落間では空白行が保たれる一方、> で始まるブロック引用符内部では
が挿入されず、段落が連続して表示されます。貼り付けは lexxy:insert-markdown イベント経由で addBlockSpacing が呼び出される設計ですが、addBlockSpacing の対象範囲が限定的だったために発生しました。このギャップ消失は、ホストアプリが期待するレイアウトを壊すため修正が必要でした。
addBlockSpacing の実装は、body > :not(h1, h2, h3, h4, h5, h6) + * という selector でトップレベル子要素のみを対象としていました。ブロック引用符内部の <p> 要素は <blockquote> の子孫であり、直接の子ではないためセレクタにマッチせず、スペーサーが挿入されませんでした。その結果、引用符内の段落は空白行なしで連結され、Markdown の意図した見た目が失われていました。したがって、ブロック引用符内の段落間に空白行を保持するロジックが求められました。
技術的な変更
セレクタを拡張し、blockquote > :not(h1, h2, h3, h4, h5, h6) + * を追加しました。この新しい selector はブロック引用符内部の隣接ブロック要素も対象に含め、従来と同様にヘディング除外条件を維持します。結果として、引用符内の段落間にも <p><br></p> のスペーサーが自動的に挿入されます。
@@
export function addBlockSpacing(doc) {
- const blocks = doc.querySelectorAll("body > :not(h1, h2, h3, h4, h5, h6) + *")
- for (const block of blocks) {
+ const selector = "body > :not(h1, h2, h3, h4, h5, h6) + *, blockquote > :not(h1, h2, h3, h4, h5, h6) + *"
+ for (const block of doc.querySelectorAll(selector)) {
const spacer = doc.createElement("p")
spacer.appendChild(doc.createElement("br"))
block.before(spacer)
}
}
テストも三点追加し、機能を検証しました。Playwright でブロック引用符の貼り付け結果を確認し、blockquote 内に二つの <p> が存在し、テキストが正しく保持されていることをアサートしています。また、addBlockSpacing が引用符内部にスペーサーを挿入することを確認するテストと、ユニットテストで同様の振る舞いを検証しています。すべてのテストが成功し、期待通りに空白行が保持されることが保証されました。
設計判断
既存の addBlockSpacing ロジックを拡張するだけのアプローチが選択されました。新規関数や大幅なリファクタリングを行わず、セレクタにブロック引用符のパスを追加することで要件を満たしています。これにより、lexxy:insert-markdown イベントを利用している既存のホストアプリケーションへの破壊的変更は発生しません。
セレクタにブロック引用符内部を含める際、ヘディング要素は除外対象と同様に除外しています。:not(h1, h2, h3, h4, h5, h6) の条件が保持されているため、見出し間に不要なスペーサーが挿入されることはありません。この設計は、他のブロック要素との一貫性を保ちつつ、意図しないレイアウト変化を回避する点で妥当です。
まとめ
今回の変更は、Markdown のブロック引用符内部で段落間の空白行が正しく保持されるよう addBlockSpacing の対象範囲を拡張したものです。最小限のコード変更で機能追加を実現し、既存の API と振る舞いを維持しながら、ホストアプリが期待するレイアウトを提供できるようになりました。