[basecamp/lexxy] Markdownとして貼り付けられたマークアップのスタイル属性をサニタイズ対象に含める
Context
Lexxyエディタでは、スタイル属性を持つHTMLをペーストした際に、カラーやハイライトなどの一部のスタイルを正規化(canonicalize)する機能があります。しかし、#602 で報告されていた通り、リッチテキストではなくMarkdownやプレーンテキストとして認識されたマークアップをペーストした場合、この正規化処理が適用されない問題がありました。
これは、insertHtml 経由で生成されたノードに PASTE_TAG が適用されていなかったことが原因です。PASTE_TAG はLexicalエディタの内部メカニズムで、ペースト操作で挿入されたノードにマークを付け、サニタイゼーション処理の対象であることを示す役割を持ちます。
Technical Detail
DOMPurifyの設定拡張
まず、HTMLサニタイゼーションライブラリであるDOMPurifyの設定に ADD_URI_SAFE_ATTR を追加し、caption と filename 属性をURI安全属性として明示的に許可するようになりました。
return {
ALLOWED_TAGS: ALLOWED_HTML_TAGS.concat(Lexxy.global.get("attachmentTagName")),
ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
ADD_URI_SAFE_ATTR: [ "caption", "filename" ],
SAFE_FOR_XML: false
}
insertHtmlメソッドのシグネチャ変更
Contents クラスの insertHtml メソッドが、オプショナルな tag パラメータを受け取れるように変更されました。このタグは editor.update() 呼び出し時に渡され、挿入されるノードにマークを付けます。
変更前:
insertHtml(html) {
this.editor.update(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) return
const nodes = $generateNodesFromDOM(this.editor, parseHtml(html))
selection.insertNodes(nodes)
})
}
変更後:
insertHtml(html, { tag } = {}) {
this.editor.update(() => {
const selection = $getSelection()
if (!$isRangeSelection(selection)) return
const nodes = $generateNodesFromDOM(this.editor, parseHtml(html))
selection.insertNodes(nodes)
}, { tag })
}
ClipboardクラスでPASTE_TAGを明示的に指定
最も重要な変更点は、Clipboard クラスの2つのメソッドで PASTE_TAG を明示的に渡すようになったことです。
import { $getSelection, $isRangeSelection, PASTE_TAG } from "lexical"
#pasteMarkdown(text) {
const html = marked(text)
this.contents.insertHtml(html, { tag: [ PASTE_TAG ] })
}
#pasteRichText(clipboardData) {
this.editor.update(() => {
const selection = $getSelection()
$insertDataTransferForRichText(clipboardData, selection, this.editor)
}, { tag: PASTE_TAG })
}
これにより、Markdown変換後のHTMLやリッチテキストがペーストされた際に、それらのノードが「ペーストされたコンテンツ」として認識され、スタイル属性のサニタイゼーション処理が正しく適用されるようになります。
テストケースの強化
今回の修正に合わせて、以下の2つのテストケースが追加されました。
test "canonicalizes styles in mark-up sent as plain-text" do
find_editor.paste %(some <span style='color: purple; background-color: rgba(229, 223, 6, 0.3);'>styled text</span>)
assert_canonicalized_to "background-color: var(--highlight-bg-1)"
end
test "canonicalizes styles in <span>" do
find_editor.paste "styled text", html: %(some <span style="color: purple; background-color: rgba(229, 223, 6, 0.3);">styled text</span>)
assert_canonicalized_to "background-color: var(--highlight-bg-1)"
end
これらのテストにより、<span> タグを含むマークアップがプレーンテキストまたはHTMLとしてペーストされた場合でも、スタイル属性が正しく正規化されることを保証しています。
設計上のトレードオフ
PR内のコメントで言及されている通り、この実装には抽象化レベルの若干の不均衡があります。理想的には以下のいずれかのアプローチが考えられました。
-
Clipboard ハンドラから
editor.update()を呼ばない:clipboardDataをContentsに渡す必要があり、抽象化が漏れる -
常に
editor.update()を呼ぶ:insertHtml内でネストした更新が発生し、パフォーマンス上好ましくない
今回採用された折衷案は、insertHtml に tag パラメータを渡すことで、呼び出し側が更新のコンテキストを制御できるようにするものです。これにより、抽象化の漏れを最小限に抑えつつ、必要な機能を実現しています。