レンダリング済みビューからのメンション貼り付けバグを修正
投稿済みコメントからコピーしたメンションを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エンティティ(< → <、" → " など)としてエンコードされます。この状態で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.js の parseAttachmentContent() 関数のコメントも実態を正確に反映するよう更新されています。もともとのコメントは「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エンティティが含まれる形式(<span ...> 等)の 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を経由したコピー&ペーストの往来が正常に機能するようになりました。