@メンション直後の不要スペースをゼロ幅文字で除去

basecamp/lexxy

@Zach's のような所有格表現でメンション末尾に余分なスペースが入る問題を、U+2060(WORD JOINER) への置き換えで解消しました。これにより、メンションと後続テキストが視覚的に隙間なく表示されます。

背景

Lexical エディタでは、インライン添付ノード(@mention)を挿入した際に後続テキストの起点として通常スペース(" ")を補完する実装になっていました。この設計は後続テキストが存在しない場合のカーソル位置確保を目的としたものですが、's のように句読点や所有格をメンション直後に入力すると、レンダリング上に不要な半角スペースが現れる副作用がありました。

同様の問題は importDOM 経由で HTML からメンションを読み込む際にも発生しており、直接入力と HTML ロードの両経路での修正が必要な状態でした。

技術的な変更

変更は src/editor/contents.jssrc/nodes/custom_action_text_attachment_node.js の2ファイルに渡り、「スペース」を「WORD JOINER」へ置き換えるという一貫したアプローチが採られています。

contents.js の変更: メンション挿入後に後続テキストノードを生成する箇所で、挿入されたノード群にインラインデコレータノードが含まれるかどうかを判定し、trailing spacer を切り替えるようにしました。

変更前:

const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor || " ")

変更後:

const trailingSpacer = this.#hasInlineDecoratorNode(replacementNodes) ? "\u2060" : " "
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor || trailingSpacer)

判定ロジックは新設のプライベートメソッド #hasInlineDecoratorNode に切り出されており、replacementNodes の中に CustomActionTextAttachmentNode のインラインインスタンスが含まれているかを Array.prototype.some で確認します。

#hasInlineDecoratorNode(nodes) {
  return nodes.some(node => node instanceof CustomActionTextAttachmentNode && node.isInline())
}

custom_action_text_attachment_node.js の変更: importDOM パスでは、HTML からパースされたノードリストに末尾スペースを追加する箇所を、同じく U+2060 へ直接置き換えています。

変更前:

nodes.push($createTextNode(" "))

変更後:

nodes.push($createTextNode("\u2060"))

これにより、エディタへの直接入力と HTML ロードの両経路で動作が統一されました。

設計判断

スペースをゼロ幅文字に置き換えるというアプローチの要点は、メンション後に何らかのテキストノードを置くという既存の構造を維持しつつ、視覚的な問題だけを取り除く点にあります。後続テキストが存在しない場合のフォールバック文字として U+2060 が採用されており、スペースを単に削除するのではなく「不可視の接続文字」へ差し替えるという最小変更で問題を解決しています。

U+2060 を選択した理由はその二つの性質、「幅ゼロで表示されない」かつ「改行禁止(WORD JOINER の名のとおり)」にあります。後者によってメンションと直後のテキストが行末で分断されないという副次的な恩恵も得られます。なお、インラインデコレータノードを含まない通常の置換では従来どおり半角スペースが使われるため、メンション以外の挿入挙動に影響はありません。

テストは Playwright による E2E テストとして追加されており、@mention 挿入後に 's を入力した際のテキストノード内容を DOM レベルで検証します。スペースで始まらないこと(/^ / に一致しないこと)と 's を含むことを明示的にアサートしており、回帰検知の網として機能します。

まとめ

ゼロ幅の WORD JOINER を trailing spacer として採用することで、既存のノード構造を変えずに視覚的な問題を解消しています。スペースの単純な削除ではなく「不可視の接続文字」への置き換えというアプローチは、エディタフレームワークの制約下での文字レベルの設計判断として参考になります。

記事メタデータ

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

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)の3部構成が明確に守られており、非常に分かりやすい構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:filepath)やGitHubのPRリンク記法([#851](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

「Lexical エディタ」「インライン添付ノード」「importDOM」などの専門用語が適切に使用されており、対象読者であるエンジニアにとって過不足のない内容です。

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

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

各セクション・各パラグラフが「総論→各論」の構成になっており、トピックセンテンスが段落の冒頭に置かれているため、非常に読みやすく理解しやすい文章構造です。

Diff内容との照合 ✓ PASS

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

記事内で引用されているすべてのコードブロックは、提供されたDiff情報と完全に一致しており、変更点が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「U+2060 (WORD JOINER)」「CustomActionTextAttachmentNode」など、PRや関連技術で用いられる用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「スペースをゼロ幅文字に置き換える」という変更の核心や、U+2060が持つ「幅ゼロ」「改行禁止」といった性質の説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の背景、修正アプローチ、テスト方法など)は、PRのDescriptionやDiffの内容によって裏付けられており、ハルシネーションは検出されませんでした。

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

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

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

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

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

記事のタイトル「@メンション直後の不要スペースをゼロ幅文字で除去」は、PRのタイトル「Remove unwanted space between @mentions and trailing text」の内容を的確に要約しています。

外部知識の正確性 ✓ PASS

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

記事内容はPR情報に忠実であり、バージョン情報やリリース予定など、PRに記載のない外部知識の追記はありません。

時間表現の正確性 ✓ PASS

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

完了した変更を説明する記事として、時間表現に誤りや歪曲は見られませんでした。