貼り付け時に入れ子リストを保持する処理を追加

basecamp/lexxy

Lexxy の貼り付け機能で、インデントされたリストが失われていた問題を修正し、入れ子 <ul>/<ol> が正しく保持されるようになりました。これにより、Gmail などからコピーした階層付き箇条書きがそのままエディタに反映されます。

背景

以前のリリース(PR #1119)で #stripStrayListChildren が導入され、 <ol>/<ul> の直下にある <li> 以外の要素(例: <br> やテキストノード)を除去することで、不要なリスト先頭の空要素を防いでいました。結果として、サブリストが親リストの直接子として存在するケース(Gmail が生成する HTML など)では、そのサブリストが削除され、階層が失われていました。複数の顧客から「インデントされた箇条書きが消える」報告が寄せられ、回帰として認識されました。

技術的な変更

src/editor/contents/pasted_content_formatter.js に新しいプライベートメソッド #nestStrayListChildren() が追加され、リスト直下にある入れ子 <ol>/<ul> を直前の <li> に移動させます。format() の実行順序も変更され、#nestStrayListChildren()#stripStrayListChildren() の前に呼び出されるようになりました。

@@
   format() {
     this.#unwrapPlaceholderAnchors()
     this.#stripTableCellColorStyles()
+    this.#nestStrayListChildren()
     this.#stripStrayListChildren()
     return this.doc
   }
@@
   // Some sources (e.g. Gmail) nest a sublist as a direct child of the parent
   // <ol>/<ul> instead of inside a <li>. Move each nested list into its
   // preceding <li> so the import preserves the nesting instead of dropping it.
   #nestStrayListChildren() {
     for (const list of this.doc.querySelectorAll("ol, ul")) {
       for (const child of Array.from(list.children)) {
         if (child.tagName !== "OL" && child.tagName !== "UL") continue

         const previousItem = child.previousElementSibling
         if (previousItem && previousItem.tagName === "LI") {
           previousItem.appendChild(child)
         }
       }
     }
   }

テストコード test/browser/tests/paste/clean_invalid_pasted_list_html.test.js にも 2 つのシナリオが追加され、直接子として配置された <ul><ol> がそれぞれ正しくネストされた HTML に変換されることを検証しています。

@@
   test.describe("Paste list with a nested list as a direct child", () => {
@@
     await editor.paste("ignored", {
       html: "<ul><li>First item</li><ul><li>Nested item</li></ul><li>Second item</li></ul>",
     })
@@
     await assertEditorHtml(
       editor,
-      '<ul><li value="1">First item<ul><li value="1">Nested item</li></ul></li><li value="2">Second item</li></ul>',
+      '<ul><li value="1">First item<ul><li value="1">Nested item</li></ul></li><li value="2">Second item</li></ul>',
     )
@@
     await editor.paste("ignored", {
       html: "<ol><li>First item</li><ol><li>Nested item</li></ol><li>Second item</li></ol>",
     })
@@
     await assertEditorHtml(
       editor,
-      '<ol><li value="1">First item<ol><li value="1">Nested item</li></ol></li><li value="2">Second item</li></ol>',
+      '<ol><li value="1">First item<ol><li value="1">Nested item</li></ol></li><li value="2">Second item</li></ol>',
     )

設計判断

#nestStrayListChildren() を追加し、既存の #stripStrayListChildren() の前に実行させるという選択は、リスト構造の保持と不要要素の除去という二つの目的を独立させるための最小限の侵襲です。メソッド自体は単純な DOM 移動ロジックだけで済み、他のフォーマッタ処理への副作用はありません。さらに、format() の呼び出し順序を変えるだけで済むため、既存のプラグインやカスタマイズとの後方互換性が保たれました。

まとめ

今回の変更により、Lexxy は直接子として配置された入れ子リストを失うことなく貼り付けられるようになり、ユーザーが期待する階層付き箇条書きの保存が可能になりました。#nestStrayListChildren() の導入は、既存のクリーンアップロジックを壊さずに機能拡張を実現した設計判断と言えるでしょう。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文がタイトル直下に配置され、背景・技術的な変更・設計判断・まとめの各セクションが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックはファイル名付きハイライトが正しく使用されていますが、PRリンクが「[PR #1156](URL)」となっており、形式は「[#1156](URL)」が推奨です。コミットIDのリンクは記載されていませんが、必須ではありません。

対象読者への適合性 ✓ PASS

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

内容はエンジニア向けで、前提知識を踏まえた説明になっており、初心者向けの過剰な解説はありません。

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

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

各セクションは総論パラグラフ→詳細パラグラフ→結論パラグラフで構成され、段落はトピックセンテンスで始まり、1段落1トピック、長さも適切です。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供されたDiffと一致し、追加・変更箇所が正確に反映されています。コメント行の一部省略は情報の欠落には当たらないと判断しました。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「nested list」「direct child」「#nestStrayListChildren」などの用語はPRで使用されているものと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

問題の背景、変更内容、効果がPRの記述と整合しており、技術的に誤った説明は見られません。

事実の突合 ✓ PASS

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

全ての主張はPRタイトル・説明・Diffで裏付けられており、外部知識の付加や推測はありません。

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

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

PR番号 #1156、ファイルパス、テストコード中のHTMLは正確です。数値の誤りはありません。

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

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

記事タイトルはPRの目的「Keep nested bullets when pasting lists」を日本語で適切に表現しています。

外部知識の正確性 ✓ PASS

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

記事に LTS やリリース日程等の外部情報は含まれておらず、PR情報のみで完結しています。

時間表現の正確性 ✓ PASS

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

時間表現の記載はなく、PRの記述と食い違う表現もありません。