HTMLインポート時のホワイトスペーステキストノードによるクラッシュを修正
メールHTMLに頻出する <div> 要素間の改行文字が引き起こす Lexical error #282 を、ルートレベルのホワイトスペース専用テキストノードをフィルタリングすることで解消しました。
背景
HEYやTutaなどのメールサービスが出力するHTMLには、<div> 要素間にインデントや改行を含むフォーマット用ホワイトスペースが含まれています。このようなHTML(#812)をLexxyエディタに読み込むと、改行文字 \n がルートレベルの TextNode として生成され、Lexicalのルートノードに直接追加できないことでクラッシュしていました。
問題を再現するHTMLは次のような構造です:
<div>First paragraph.</div>
<div><br></div>
<div>Second paragraph.</div>
この \n が $generateNodesFromDOM によってルートレベルの TextNode に変換され、root.append() の段階で Uncaught Error: Minified Lexical error #282 をスローしていました。同じ内容を <p> タグで記述した場合には発生せず、<div> と <p> の処理に一貫性がないことも問題でした。
技術的な変更
src/elements/editor.js の HTMLインポートパイプラインに、ホワイトスペース専用ノードを除去するフィルタステップが追加されました。
変更前:
return nodes
.map(this.#wrapTextNode)
.map(this.#unwrapDecoratorNode)
変更後:
return nodes
.filter(this.#isNotWhitespaceOnlyNode)
.map(this.#wrapTextNode)
.map(this.#unwrapDecoratorNode)
フィルタ関数 #isNotWhitespaceOnlyNode は2つのケースを除外します。まず $isLineBreakNode が true のノードは無条件に除去します。次に $isTextNode かつ node.getTextContent().trim() === "" となる空白のみのテキストノードも除去します。これらはHTMLソースのフォーマット由来のアーティファクトであり、ルートノードへの追加もセマンティクスも持たないと明示的にコメントで説明されています。
また $isLineBreakNode を利用するため、importに $isLineBreakNode が追加されています。テストは test/browser/tests/editor/load_html.test.js に追加され、改行入りの <div> HTMLを読み込んでもクラッシュせず、段落テキストが正しく表示されることを検証しています。
設計判断
フィルタをパイプラインの先頭に配置する方針が選ばれました。#wrapTextNode はテキストノードを <p> でラップする処理ですが、ホワイトスペース専用のテキストノードをラップしても意味のない空段落が生成されます。先にフィルタリングすることで、後続の変換処理が意味のあるノードだけを扱えるようになっています。
<p> では発生しなかった問題が <div> で発生していた理由は、既存の処理パスの差異にあります。今回の修正はその差異を解消するのではなく、ルートレベルへの不正なノード追加そのものを上流で遮断するアプローチです。ホワイトスペースノードはセマンティクスを持たないという前提を #isNotWhitespaceOnlyNode に集約したことで、将来的に除外条件を追加する際の変更箇所も明確になっています。
まとめ
HTMLインポートパイプラインへのフィルタ追加という最小限の変更で、メールHTML由来のフォーマット用ホワイトスペースに起因するクラッシュを根本から解消しました。<div> と <p> の処理の一貫性も確保され、エディタの入力データの品質に依存した脆弱性が取り除かれています。