メンション後の不可視文字(U+2060)を除去し、スペース管理を整理
メンション挿入時に付与されていたワードジョイナー文字(U+2060)を削除し、通常のスペースと条件付きロジックによるシンプルな後処理に置き換えました。これにより、メンション後のカーソル操作や編集時の意図しない動作が解消されます。
背景
#851 で「メンションと後続テキストの間に不要なスペースが入る」問題を修正するため、末尾スペースの代替としてゼロ幅のワードジョイナー文字 \u2060 が導入されました。しかし、この文字が逆にタイピングや編集時の意図しない動作を引き起こしていました。具体的には、エディタ上でカーソルがメンション直後の不可視文字に引っかかる現象や、Backspace キー操作が余分な削除ステップを必要とする状況が生じていました。
本PRはこの \u2060 を完全に取り除き、スペースの挿入をより明示的な条件ロジックで管理する方針に切り替えています。
技術的な変更
変更は大きく「\u2060 の除去」と「スペース挿入ロジックの整理」の2軸で行われています。
U+2060の除去と条件付きスペース挿入
src/editor/contents.js では、\u2060 を条件分岐で使い分けていた trailingSpacer 変数が削除されました。
変更前:
const trailingSpacer = this.#hasInlineDecoratorNode(replacementNodes) ? "\u2060" : " "
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor || trailingSpacer)
変更後:
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor || " ")
これに伴い、#hasInlineDecoratorNode プライベートメソッドも不要になり削除されています。
importDOMパスでのスペース挿入の整理
src/nodes/custom_action_text_attachment_node.js の importDOM パスでも、固定で \u2060 を挿入していた箇所が、後続の実際のDOMノードを確認してスペースを挿入する条件付きロジックに変わりました。
変更前:
nodes.push($createTextNode("\u2060"))
変更後:
const nextSibling = attachment.nextSibling
if (nextSibling && nextSibling.nodeType === Node.TEXT_NODE && /^\s/.test(nextSibling.textContent)) {
nodes.push($createTextNode(" "))
}
HTMLからメンションをロードする際、後続のテキストノードが実際に空白で始まる場合にのみスペースを追加します。これにより、元のHTMLの空白状態を忠実に反映した読み込みが可能になります。
toString時のスペーサーノードのスキップ
src/elements/editor.js の $getReadableTextContent 関数に、メンション直後のスペーサー用テキストノードをスキップする処理が追加されました。src/helpers/lexical_helper.js に新設された isAttachmentSpacerTextNode ヘルパーがその判定を担います。
export function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
return $isTextNode(node)
&& node.getTextContent() === " "
&& index === childCount - 1
&& previousNode instanceof CustomActionTextAttachmentNode
}
このヘルパーは「テキストノードであること」「内容が半角スペース1文字であること」「末尾の子ノードであること」「直前が CustomActionTextAttachmentNode であること」の4条件をすべて満たす場合にスペーサーと判定します。この結果、editor.toString() の出力からスペーサーが除外され、test/system/editor_to_string_test.rb のアサーションも "Peter Johnson\u2060" から "Peter Johnson" に修正されています。
あわせて、toString 内の cachedStringValue の存在チェックが !this.cachedStringValue から this.cachedStringValue == null に変更されました。これにより、set value() 呼び出し後に空文字列 "" が設定された場合でも、falsy値として誤って再計算されることなく空文字列が正しく返されます。
テストの修正
test/browser/tests/attachments/mentions.test.js では、's を入力して前後スペースがないことを検証するテストが、「メンション直後にスペースが存在すること」を検証するテストに書き換えられました。また、test/browser/helpers/editor_handle.js に #translateKey メソッドが追加され、macOSで Home/End キーがページスクロールに割り当てられる問題を Meta+ArrowLeft/Meta+ArrowRight に変換することでクロスプラットフォームのテスト動作を保証します。
設計判断
不可視文字による回避策よりも、明示的なスペース管理を選択した点がこのPRの核心です。
\u2060 は「見えない文字でスペースの問題を隠す」アプローチでしたが、Lexicalのエディタ状態においては不可視であっても実在するテキストノードとして振る舞います。カーソル移動や Backspace の動作、toString() の出力といった操作のすべてで考慮が必要になり、むしろ複雑性が増していました。isAttachmentSpacerTextNode のような専用ヘルパーで「スペーサーである」という意図を明示的にコードで表現したことで、各処理ポイントで意識的に扱えるようになっています。
importDOM パスにおいて固定挿入をやめて後続DOMノードを確認する方式を採用したのも同様の判断です。元のHTMLが持つ空白の有無を尊重することで、ロード時と入力時の挙動の一貫性が高まります。
まとめ
\u2060 という不可視文字への依存を断ち切り、スペースの存在をコード上で明示的に管理する設計への転換が本PRの本質です。isAttachmentSpacerTextNode という小さなヘルパーの導入により、エディタ内容の読み取り・表示・テストの各レイヤーで一貫した処理が可能になっています。