HTMLペーストでプレースホルダーアンカーを除去し、実リンクと@メンションを正確に保持
レンダリング済みコンテンツからコピー&ペーストする際に、<a href="#"> や <a href=""> として実装されているプレースホルダーアンカーをプレーンテキストに変換しつつ、意味のある実URLを持つリンクは維持するようになりました。
背景
編集状態でない通常の表示画面(レンダリング済みビュー)からコンテンツをコピーすると、@メンションやインタラクティブ要素が <a href="#"> タグでラップされた状態でクリップボードに入ります。これをエディタにペーストすると、href="#" を持つ空のリンクノードが生成され、表示と構造が意図しない状態になるという問題がありました(Fizzy card #3785)。
href="#" や href="" は、JavaScriptで動作するUI要素(メンションのポップアップ表示など)のためにHTMLの慣例として使われるプレースホルダーです。こうした要素は「リンク」ではなくテキストコンテンツとしてペーストされるべきですが、これまでのペースト処理ではアンカータグの有無だけでリンクノードへの変換が判定されていたため、プレースホルダーも実リンクも区別なく処理されていました。
技術的な変更
src/editor/contents.js に #unwrapPlaceholderAnchors プライベートメソッドが追加され、insertDOM の冒頭でDOMを操作する前に呼ばれるようになりました。
変更前:
insertDOM(doc, { tag } = {}) {
this.editor.update(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) return
変更後:
insertDOM(doc, { tag } = {}) {
this.#unwrapPlaceholderAnchors(doc)
this.editor.update(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) return
追加されたメソッドの実装は以下のとおりです。
#unwrapPlaceholderAnchors(doc) {
for (const anchor of doc.querySelectorAll("a")) {
const href = anchor.getAttribute("href") || ""
if (href === "" || href === "#") {
anchor.replaceWith(...anchor.childNodes)
}
}
}
querySelectorAll("a") で全アンカーを走査し、href が空文字または "#" の場合のみ replaceWith(...anchor.childNodes) で自身をその子ノード群に置き換えます。これにより、@Jorge のようなテキストコンテンツはそのまま保持されつつ、アンカータグだけが除去されます。href に実URLを持つアンカーはこの条件に合致しないため、リンクノードとして変換される従来の処理へとそのまま流れます。
テストは test/browser/tests/paste/links.test.js に3ケースが追加されました。実URLのみ、href="#" のみ、そして両者が混在するケースをそれぞれカバーし、混在ケースでは実リンクが保持されつつプレースホルダーがプレーンテキスト化されることを確認しています。
設計判断
Lexical(エディタフレームワーク)のノード変換処理に入力するDOM自体を前処理する方式 が採用されました。
Lexicalのペースト処理はDOMをノードツリーに変換しますが、この変換ロジックを変更するのではなく、変換前のDOMを正規化するアプローチが取られています。insertDOM の冒頭でプレースホルダーアンカーを除去することで、Lexical側の変換ロジックからは「最初からアンカーが存在しなかった」状態に見えます。これにより、フレームワーク内部のリンク変換ロジックに手を加えることなく問題を解決しています。
また、getAttribute("href") || "" という記述により、href 属性が存在しない場合も同一の条件式で処理できます。プレースホルダーとして扱う条件を "" と "#" の2値に限定することで、誤って実リンクを除去するリスクを最小化しています。
まとめ
この変更は、レンダリング済みビューとエディタの間でコンテンツをやり取りする際のインピーダンスミスマッチを、DOM前処理というシンプルな手法で解消しています。フレームワークの変換パイプラインに触れず、入力の正規化だけで問題を解決した点は、変更の局所性と保守性の観点で優れた設計判断です。