`permitted-attachment-types` によるエディタ境界での添付ファイル制御
<lexxy-editor> に data-permitted-attachment-types 属性を宣言するだけで、ファイルドロップ・HTMLインポート・プロンプト起動・プログラム挿入の全経路にわたって添付ファイルのコンテンツタイプを一括制限できるようになりました。ホストアプリがアタッチメントの種別を自前でフィルタリングする必要はなくなります。
背景
Lexxyを利用するホストアプリには「メンションとOGエンベッドは許可しつつ、ファイルアップロードは禁止したい」というフォーム単位の要件が長年存在していました。しかし既存の attachments=false は添付ファイル全体のオン/オフしか制御できず、種別ごとの許可リストはホスト側が独自に実装するほかありませんでした。保存済みのメンションやOGエンベッドを再編集する際も、インポートパスでのフィルタリングが欠落していたため、ラウンドトリップが壊れるバグが発生していました。
このPRはその課題を解消するため、コンテンツタイプの許可リストをエディタ要素自身が保持し、すべての添付エントリポイントでその判定を一元的に参照する設計を採用しています。
技術的な変更
permittedAttachmentTypes / permitsAttachmentContentType の追加
LexicalEditorElement(src/elements/editor.js)に2つの公開APIが追加され、許可リストのパースと判定が一箇所に集約されました。
get permittedAttachmentTypes() {
const raw = this.config.get("permittedAttachmentTypes")
if (raw == null) {
return null
} else {
const tokens = Array.isArray(raw) ? raw : String(raw).split(/\s+/)
return Object.freeze(tokens.filter(t => t && t !== "false"))
}
}
permitsAttachmentContentType(contentType) {
if (!this.supportsAttachments) {
return false
} else {
const list = this.permittedAttachmentTypes
return list === null || list.includes(contentType)
}
}
permittedAttachmentTypes は属性が未設定の場合に null を返し、設定されている場合はスペース区切りのトークンリストを凍結配列で返します。permitsAttachmentContentType は supportsAttachments(attachments=false のチェック)を先に評価し、その後で許可リストを参照します。これにより attachments=false と空の許可リストが同一の振る舞いを示します。後方互換性のため、レガシーな "false" 文字列トークンをフィルタして空リスト相当に変換する処理も含まれています。
$generateFilteredNodesFromDOM ヘルパー
HTMLインポートパスに新しいフィルタリングレイヤーが追加されました。新設の src/helpers/attachment_filter_helper.js が、@lexical/html の $generateNodesFromDOM をラップします。
export function $generateFilteredNodesFromDOM(editorElement, doc) {
const nodes = $generateNodesFromDOM(editorElement.editor, doc)
return filterDisallowedAttachmentNodes(nodes, editorElement)
}
export function filterDisallowedAttachmentNodes(nodes, editorElement) {
return nodes
.filter(node => !isDisallowedAttachment(node, editorElement))
.map(node => {
$descendantsMatching([ node ], descendant => isDisallowedAttachment(descendant, editorElement))
.forEach(descendant => descendant.remove())
return node
})
}
トップレベルノードのフィルタリングに加え、$descendantsMatching で子孫ノードを再帰的に走査することで、テーブルセル内にネストされた添付ファイルも確実に除去されます。contents.js・editor.js・prompt.js の各コールサイトで $generateNodesFromDOM への直接呼び出しがこのヘルパーに置き換えられました。
ファイルドロップ・ペーストのフィルタリング
エディタ初期化時に lexxy:file-accept イベントの内部リスナーが登録されます。MIMEタイプが許可リストに含まれない場合、このリスナーが event.preventDefault() を呼び出してアップロードをキャンセルします。内部リスナーは lexxy:initialize イベントより前にインストールされるため、ホストが initialize ハンドラで独自リスナーを後から登録した場合は「最後に登録したリスナーが最終決定権を持つ」セマンティクスが維持されます。
プロンプト起動の制御
src/elements/prompt.js の #addTriggerListener 冒頭に #promptContentTypePermitted ゲートが追加されました。
#addTriggerListener() {
if (!this.#promptContentTypePermitted) return
// ...
}
get #promptContentTypePermitted() {
const el = this.#editorElement
if (!el.supportsAttachments) {
return false
} else {
const templates = Array.from(this.querySelectorAll("template[type='editor']"))
const types = templates.length
? templates.map(t => t.getAttribute("content-type") || this.#defaultPromptContentType)
: [ this.#defaultPromptContentType ]
return types.some(t => el.permitsAttachmentContentType(t))
}
}
プロンプトが持つすべての <template type="editor"> の content-type を評価し、いずれか1つでも許可されていればプロンプトが起動します。許可されない場合はトリガーリスナー自体が登録されないため、ポップオーバーがDOMに現れることはありません。
プログラム挿入のフォールバック
lexxy:insert-link イベントの replaceLinkWith / insertBelowLink コールバックは、#createCustomAttachmentNodeWithHtml を経由して添付ノードを生成していました。このメソッドが permitsAttachmentContentType を参照するように変更され、コンテンツタイプが不許可の場合は #createHtmlNodeWith へフォールバックします。フォールバック先も $generateFilteredNodesFromDOM を経由するため、フォールバックHTMLの中に不許可の添付タグが含まれていても除去されます。
Rubyサイドの content-type 伝播
lib/lexxy/rich_text_area_tag.rb に1行の修正が加わり、<action-text-attachment> ノードに content-type 属性が付与されていない場合でも、Attachmentオブジェクトから content_type を補完するようになりました。
node["content-type"] ||= attachment.content_type
これにより tag_helper_test.rb のアサーションが content-type 属性の存在を検証するよう更新されています。
設計判断
単一の述語メソッドを全エントリポイントが参照する設計が採用されました。フィルタリングロジックが permitsAttachmentContentType に一元化されているため、将来エントリポイントが増えた場合も判定ロジックの重複が生じません。
属性のパースは connectedCallback 時に一度だけ行い結果をキャッシュするというLexxy既存の初期化規約に揃えた点も注目に値します。これは「設定変更が必要な場合は要素の再接続で対応する」という設計方針の一貫性を保つためです。動的な許可リスト変更(attributeChangedCallback による再パース)を採用しなかったことで、実行時の状態管理が単純になっています。
attachments=false と空の許可リストが同一の振る舞いをとる設計も意図的です。これにより既存の attachments=false コードパスはそのまま維持しつつ、permitsAttachmentContentType という単一のゲートで両方のケースを統一的に扱えます。
まとめ
本PRは、エディタ要素自身が添付ファイルの許可リストを保持し、インポート・ドロップ・プロンプト・プログラム挿入の4経路すべてで同一の述語を参照することで、ホスト側の実装コストを排除した変更です。permitted-attachment-types を宣言するだけでラウンドトリップを含む全経路が一貫して制御され、既存の attachments=false との後方互換性も維持されています。