テーブルセルの背景色を正規化し、貼り付け・ロード時の外部カラーを除去

basecamp/lexxy

Lexxy のテーブルで、外部コンテンツ(Excel など)から持ち込まれたセルの背景色や文字色が、貼り付け後だけでなく保存・再編集時にも残っていた問題を、セル変換で背景色を常に空文字列にリセットすることで統一的に解決しました。

背景

Lexical はセルの background-colorTableCellNode の state として保持し、インポート時にインラインスタイルから取得します。貼り付け時に実行される PastedContentFormatter はセル色を除去しますが、setValue でロードされる保存済みコンテンツには適用されず、結果として外部色がエディタ → 保存 → 再表示 → 再編集のサイクルを通過します。さらに、既存の TableCellNode 変換は null のときだけ背景をクリア する回避策(Lexical のハードコードされたヘッダー背景色問題 #8089)であり、実際には外部色が残る抜け穴となっていました。

技術的な変更

src/extensions/tables_extension.js において、すべての TableCellNode を対象に背景色を正規化するノード変換が追加されました。コメントも更新され、セルの背景は「外部コンテンツからのみ来る」前提で 常に空文字列にリセット する旨が明記されています。

@@ -55,9 +55,13 @@ export class TablesExtension extends LexxyExtension {
             }
           }),

-          // Bug fix: Prevent hardcoded background color (Lexical #8089)
+          // A table in Lexxy is a Lexxy table: cell shading can't be set in the
+          // editor, so any cell background only ever comes from foreign content
+          // (pasted spreadsheets, loaded documents). Normalize every cell to no
+          // background so it adopts the current theme. This also clears
+          // Lexical's hardcoded default header background (Lexical #8089).
           editor.registerNodeTransform(TableCellNode, (node) => {
-            if (node.getBackgroundColor() === null) {
+            if (node.getBackgroundColor() !== "") {
               node.setBackgroundColor("")
             }
           }),

変更前は node.getBackgroundColor() === null のときだけ setBackgroundColor("") が実行されていましたが、!== "" という条件に改めることで、背景色が空文字列であっても(実際には外部スタイルが設定されているケース)必ずクリアされます。これにより、Lexical のデフォルトヘッダー背景色と、貼り付け・ロードされたセルの外部シェーディングの両方が一括で除去されます。

テストスイートも拡充され、test/browser/tests/paste/table_cell_styles.test.js に以下のシナリオが追加されました。

// Excel がセル内部の span/font にハードコードした文字色も除去するテスト
test("strips color from elements nested inside pasted table cells", async ({
  editor,
}) => {
  const tableHtml = `
    <table>
      <tr>
        <td style="color: windowtext;"><span style="color: black;">First</span></td>
        <td><font color="#000000"><span style="color: #000000;">Second</span></font></td>
      </tr>
    </table>
  `;

  await editor.paste("First\tSecond", { html: tableHtml });
  // すべてのセルと子要素の color スタイルが空になることを期待
});

// Excel 風のシェーディングセルが貼り付け時に背景と文字色が除去されるテスト
test("strips shading and text color from an Excel-style shaded cell", async ({
  editor,
}) => {
  const tableHtml = `
    <table>
      <tr>
        <td style="background-color: #ffff00; color: black;"><span style="color: black;">Shaded</span></td>
      </tr>
    </table>
  `;

  await editor.paste("Shaded", { html: tableHtml });
});

これらのテストは、保存済みコンテンツのロード時貼り付け時にセル内にネストした要素、そして Excel 風のシェーディングセル すべてで色が除去されることを検証しています。

設計判断

セル背景の正規化は新しいサニタイズロジックを別途導入するのではなく、既存の TableCellNode ノード変換 に統合された点が重要です。これにより、貼り付け・ロード・再編集の三つのパスに対して同一ロジックが適用され、コードベースの重複を防ぎつつメンテナンスコストを抑えられます。

背景色をクリアするだけでテーマカラーはそのまま継承されるため、Lexxy の テーマ一貫性 が保たれます。文字色やその他のインラインスタイルは パレットハイライト が内部ノードで管理しているため影響を受けず、ユーザーが意図したテキスト装飾は残ります。

セルシェーディング自体がエディタ側で設定できない方針(「セルの背景は外部コンテンツの影響下にある」)と整合させるため、背景のみを空文字列にリセット するという最小限の変更にとどめた設計です。これにより、将来的に背景以外の属性を拡張する際にも影響範囲が限定的です。

まとめ

TableCellNode の変換ロジックを改修し、すべてのセル背景を空文字列に正規化したことで、貼り付け・保存・再読み込みのすべてのフローで外部カラーが残らないようになりました。結果として、Lexxy のテーブルはテーマに忠実に表示され、不要なセルシェーディングや内部文字色が除去されることで、コンテンツの一貫性と編集体験が向上します。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
dd31ddac

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文がタイトル直下にあり、背景、技術的な変更、設計判断、まとめの各セクションが揃っている。まとめは単なる繰り返しでなく、全体の意義を述べている。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは適切に diff と javascript のシンタックスハイライトで記載され、GitHubリンクは PR 番号・Issue 番号ともに正しい形式でリンク化されている。ファイル名付きハイライトが不要なため問題なし。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

対象は Lexical/Lexxy を扱うエンジニアであり、専門用語や実装詳細が中心。初心者向けの過剰な説明はなく、適切なレベルで記述されている。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論→各論→結論の構成になり、段落はトピックセンテンスで始まり 1 段落 1 トピックで書かれている。段落長も 6 文以内で分割され、空行で区切られている。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中の diff コードブロックは提供された Diff と完全に一致しており、変更箇所・条件式の修正が正しく反映されている。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

TableCellNode、backgroundColor、Lexical #8089 などの用語が PR と一致し、誤用は見られない。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的主張(背景色を空文字列に正規化する意図と効果)は PR の説明とコード変更に裏付けられており、論理的に正しい。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての主張は PR タイトル・説明・Diff に基づいており、根拠のない推測や外部知識の付加はない。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #1174、Issue 番号 #8089 などの数値・固有名詞が正確に記載されている。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

日本語タイトルは PR の内容(外部セルのシェーディング除去)を正しく要約している。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事中に PR に記載されていないバージョン情報やリリーススケジュール等の外部知識は含まれていない。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PR の記述と一致している。