ペーストされたdata-URI画像をアップロードノードへ自動変換
Google Docsなどからリッチテキストをペーストすると、<img src="data:image/png;base64,..."> 形式のインライン画像が混入し、保存されたHTMLを肥大化させる問題がありました。本PRでは、ペースト時にこれらのdata-URI画像を検出し、通常のファイルアップロードパイプライン(Active Storage)へ自動的に変換する機能を実装しています。
背景
リッチテキストエディタのペースト処理において、data-URI形式のインライン画像はドキュメントサイズを著しく肥大化させる問題を引き起こします。Google Docsなど多くのオフィスアプリケーションは、コンテンツのコピー時に画像をbase64エンコードしてHTMLに直接埋め込みます。これがエディタにペーストされると、base64ペイロードがそのまま保存HTMLに永続化され、ドキュメントのストレージコストが増大します。
PR本文では、同様の問題をTrixエディタ向けに実装した Basecampの過去の取り組み を参照しており、Lexxyでも同等の保護機構が必要であると判断されたことがわかります。
技術的な変更
本PRの中心となる変更は、新規ファイル src/helpers/inline_image_uri_helper.js の追加と、ノード生成処理の責務の再編成です。
$convertInlineImageDataURIs 関数が新設され、ペースト時にのみ呼び出されます。処理の流れは以下の通りです:
- トップレベルノードを走査し、
data:image/...;base64,にマッチするActionTextAttachmentNodeを検出 - base64データをデコードして
Fileオブジェクトを生成 - キャンセル可能な
lexxy:file-acceptイベントを発火し、ホスト側のファイル許可リストによるフィルタリングを実施 - 受理された場合は
ActionTextAttachmentUploadNodeに置換し、通常のActive Storageアップロードフローへ流す - 拒否された場合はノードを静かに除去
また、$generateFilteredNodesFromDOM というヘルパー関数が廃止され、LexicalEditorElement のメソッド $generateNodesFromDOM に統合されました。
変更前:
export function $generateFilteredNodesFromDOM(editorElement, doc) {
const nodes = $generateNodesFromDOM(editorElement.editor, doc)
return filterDisallowedAttachmentNodes(nodes, editorElement)
}
変更後:
$generateNodesFromDOM(doc) {
let nodes = $generateLexicalNodesFromDOM(this.editor, doc)
if ($hasUpdateTag(PASTE_TAG)) nodes = $convertInlineImageDataURIs(nodes, this)
return filterDisallowedAttachmentNodes(nodes, this)
}
PASTE_TAG の有無を確認することで、ペースト操作時のみdata-URI変換が行われ、HTMLのロード時(setValue 経由など)には変換が実行されないことを保証しています。この挙動は追加されたテスト "preserves inline image data URIs untouched (no paste-time conversion)" で明示的に検証されています。
さらに、アップロードノードの生成ロジックが $createUploadNode として Contents クラスのメソッドに抽出されました。
変更前(Uploader クラス内に散在):
$createUploadNodes() {
this.nodes = this.files.map(file =>
$createActionTextAttachmentUploadNode({
...this.#nodeUrlProperties,
file: file,
contentType: file.type
})
)
}
get #nodeUrlProperties() {
return {
uploadUrl: this.editorElement.directUploadUrl,
blobUrlTemplate: this.editorElement.blobUrlTemplate
}
}
変更後(Contents クラスに集約):
$createUploadNode(file) {
return $createActionTextAttachmentUploadNode({
file,
uploadUrl: this.editorElement.directUploadUrl,
blobUrlTemplate: this.editorElement.blobUrlTemplate,
contentType: file.type,
})
}
この変更により Uploader クラスは this.contents.$createUploadNode(file) を呼び出すだけになり、URLプロパティの取得ロジックが一箇所に集約されました。
設計判断
ペースト操作のみに変換を限定する 設計が採用されています。$hasUpdateTag(PASTE_TAG) によるガードにより、HTMLのプログラム的なロード時にはdata-URIがそのまま保持されます。これは、既存コンテンツへの意図しない変換を防ぎつつ、ユーザーの貼り付け操作によるデータ流入のみを対象とする明確な境界設定です。
data-URI変換の拒否フローは、既存のファイルピッカーによるファイルアップロード拒否と同じ lexxy:file-accept イベントを経由します。これにより、ホスト側は画像ソースの違いを意識せず単一のイベントハンドラで許可制御を実装できます。また拒否時にエラーを発生させず静かにノードを除去する動作も、ファイルピッカー拒否時の既存挙動に揃えたものです。
テストファイル test/browser/tests/paste/data_uri_safeguards.test.js は223行にわたり、単独画像・周囲コンテンツとの位置関係・MIMEタイプの保持・拒否フロー・SVGなど複数のケースを網羅しています。また、eslint.config.js に atob: "readonly" が追加されており、base64デコードに使用するグローバル関数をESLintに認識させています。
まとめ
ペーストによるdata-URI画像の混入という具体的な問題に対し、変換処理をペースト操作スコープに限定し、既存の lexxy:file-accept イベントによる許可制御を再利用することで、最小限の変更範囲で解決しています。$generateNodesFromDOM への責務統合と $createUploadNode の抽出は、新機能の追加と同時にコードの重複を除去するリファクタリングも兼ねており、今後のアップロードパイプライン拡張の基盤を整えています。