レンダリング済みビューからのメンション貼り付けバグを修正

basecamp/lexxy

投稿済みコメントからコピーしたメンションをLexxyエディタに貼り付けると文字化けが発生していた問題を修正しました。原因は exportDOM() での JSON.stringify() 使用によるHTMLエンティティとJSONエスケープの衝突であり、Trix/ActionText形式に合わせた生HTML保存に変更することで解消されています。

背景

Lexxyエディタは action-text-attachment カスタム要素を通じてメンションを管理しており、content 属性にメンションのHTMLを保持します。この content 属性の形式として、LexxyはTrix/ActionTextとは異なる方式を採用していました。

具体的には、Lexxyの exportDOM()content 属性の値を JSON.stringify() でラップして出力していました。一方でTrix/ActionTextは生のHTMLをそのまま格納します。RailsのビューでレンダリングされたHTMLをブラウザがコピーする際、属性値はHTMLエンティティ(<&lt;"&quot; など)としてエンコードされます。この状態でJSONのバックスラッシュエスケープが混在すると、ペースト時の復元処理で二重エンコードが発生し、content 属性の内容が壊れていました。

この問題はFizzy(内部Issue管理)のカード #3251 として報告されており、「投稿済みコメントからメンションをコピーしてLexxyエディタに貼り付けると、機能するメンションではなく文字化けしたテキストが生成される」という症状でした。

技術的な変更

変更の核心は exportDOM() における content 属性の生成方法の修正です。JSON.stringify() を取り除き、生HTMLをそのまま格納するようにしました。

変更前:

exportDOM() {
  const attachment = createElement(this.tagName, {
    sgid: this.sgid,
    content: JSON.stringify(this.innerHtml),
    "content-type": this.contentType
  })

変更後:

exportDOM() {
  const attachment = createElement(this.tagName, {
    sgid: this.sgid,
    content: this.innerHtml,
    "content-type": this.contentType
  })

これに合わせて、src/helpers/storage_helper.jsparseAttachmentContent() 関数のコメントも実態を正確に反映するよう更新されています。もともとのコメントは「LexxyはJSON文字列でエクスポートし、Trix/ActionTextは生HTMLで格納するため、まずJSONを試みてから生HTMLにフォールバックする」という記述でしたが、修正後は「content 属性は生HTML(Trix/ActionText形式に準拠)であり、古いLexxyバージョンとの後方互換性のためにまず JSON.parse を試みる」という記述に変わっています。

// The content attribute is raw HTML (matching Trix/ActionText). Older Lexxy
// versions JSON-encoded it, so try JSON.parse first for backward compatibility.
export function parseAttachmentContent(content) {
  try {
    return JSON.parse(content)

parseAttachmentContent() のロジック自体は変更されておらず、まず JSON.parse を試み、失敗した場合は生HTMLにフォールバックするという処理がそのまま維持されています。これにより、過去にJSON形式で保存された既存データも引き続き正常に読み込めます。

テストとして、レンダリング済みビューからコピーしたHTMLを模擬したペーストシナリオが test/browser/tests/paste/paste.test.js に追加されました。テストケースでは content 属性にHTMLエンティティが含まれる形式(&lt;span ...&gt; 等)の action-text-attachment HTML文字列を直接ペーストし、エディタ内に1件の action-text-attachment が正しく挿入されることを検証しています。

設計判断

Trix/ActionText形式への準拠という方向性が採用されました。

新しい出力形式を設けるのではなく、Trix/ActionTextが既に確立している形式(content 属性に生HTMLを格納)に合わせることで、ブラウザ・Rails・Lexxy間のデータの往来におけるエンコード処理の複雑さを排除しています。parseAttachmentContent()JSON.parse のフォールバックが元々実装されていたことが、後方互換性を保ちながら今回の修正を可能にした重要な前提となっています。

つまり、「読み込み側が両形式を吸収できる」という設計が先にあったため、「書き出し側をシンプルな正規形に統一する」変更を安全に行えた構造です。

まとめ

本PRは JSON.stringify() という1行の変更と、もともと実装されていたフォールバック処理の活用によって、ブラウザのHTMLエンティティエンコードとJSONエスケープの衝突を根本から解消しています。exportDOM() の出力をTrix/ActionText形式に統一したことで、既存データへの後方互換性を維持しながら、Railsを経由したコピー&ペーストの往来が正常に機能するようになりました。

記事メタデータ

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

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)」という3部構成が明確に守られています。各セクションの役割がはっきりしており、非常に分かりやすい記事構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト(```javascript:path/to/file.js)およびPR番号のリンク記法([PR #898](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

「HTMLエンティティ」「JSONエスケープ」「Trix/ActionText」などの専門用語を前提としており、対象読者であるエンジニアに適した技術レベルで書かれています。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まっています。1段落1トピックの原則も守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

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

`exportDOM`関数の変更点や`storage_helper.js`のコメント更新など、記事内のコード引用と説明は提供されたDiff情報と正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「exportDOM」「action-text-attachment」「HTMLエンティティ」などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

HTMLエンティティとJSONエスケープの衝突という問題の根本原因、および後方互換性を維持しつつ修正した方法についての技術的な説明が正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の背景、修正内容、設計判断)は、PRのDescriptionやDiff内容によって裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#898)および関連するIssue番号(#3251)が正確に記載されています。

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

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

記事のタイトル「レンダリング済みビューからのメンション貼り付けバグを修正」は、PRのタイトル「Fix mention pasting from rendered views」の内容を忠実に反映しています。

外部知識の正確性 ✓ PASS

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

PR情報に記載のない、バージョンのサポート状況やリリース予定などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

「発生していた問題」「もともと実装されていた」など、変更の前後関係を示す時間表現がPRの内容と一致しており、正確です。