メンション後の不可視文字(U+2060)を除去し、スペース管理を整理

basecamp/lexxy

メンション挿入時に付与されていたワードジョイナー文字(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.jsimportDOM パスでも、固定で \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 という小さなヘルパーの導入により、エディタ内容の読み取り・表示・テストの各レイヤーで一貫した処理が可能になっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
eada87fc

この記事は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

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

Lexicalエディタの内部実装に関する専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に適合した内容です。

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

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

各セクションが総論・各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事で引用されているすべてのコードブロックとファイル名は、提供されたDiff情報と正確に一致しています。変更内容の説明もDiffと整合性が取れています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「ワードジョイナー文字(U+2060)」「DecoratorNode」「importDOM」などの技術用語が文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

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

不可視文字が引き起こす問題や、`cachedStringValue`のチェックロジック変更の理由など、技術的な説明が正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiff内のコード変更によって裏付けられており、ハルシネーション(創作)は見られません。

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

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

PR番号(#876, #851)や文字コード(U+2060)などの数値・固有名詞はすべて正確に記載されています。

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

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

記事のタイトル「メンション後の不可視文字(U+2060)を除去し、スペース管理を整理」は、PRのタイトル「Mention glue removal」の内容を的確かつ具体的に要約しています。

外部知識の正確性 ✓ PASS

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

PR情報に含まれないバージョンサポート状況やリリース日程といった外部知識の追記はなく、提供された情報に忠実です。

時間表現の正確性 ✓ PASS

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

過去の変更(#851での導入)と今回の変更の関係性など、時間表現に歪曲や誤りはなく、事実関係が正確に記述されています。