HTML出力から冗長なbold/italicタグを除去するカスタムexportDOMを追加

basecamp/lexxy

LexicalのTextNode.exportDOM()が生成する<b><strong>text</strong></b><i><em>text</em></i>という二重タグ問題を、カスタムexport関数で解消しました。これにより、HTML出力のセマンティクスが正しい形に整理されます。

背景

LexicalのTextNodeはHTML出力時に、セマンティクスタグと表示用タグを二重に付与する問題を抱えていました。具体的には、createDOM()<strong>(bold)や<em>(italic)といったセマンティクスタグを生成した上で、exportDOM()がさらに<b><i>といった表示用タグで無条件にラップするため、<b><strong>text</strong></b>という冗長なマークアップが出力されていました。

この問題はPlaywrightのテストやCapybaraのシステムテストにも反映されており、たとえばtest/browser/tests/formatting/inline_formatting.test.jsでは"<p>Hello <b><strong>everyone</strong></b></p>"が期待値として記述されていました。出力HTMLをそのままTrixなど他のエディタに渡す際や、アクセシビリティの観点から、冗長なタグは除去されるべきものでした。

技術的な変更

新たに追加されたsrc/helpers/text_node_export_helper.jsexportTextNodeDOM 関数が、Lexical組み込みのTextNode.exportDOM()を置き換えます。この関数はcreateDOM()が生成した要素を起点とし、各フォーマットについてセマンティクスタグの重複をチェックしてからラップするかどうかを判断します。

export function exportTextNodeDOM(editor, textNode) {
  const element = textNode.createDOM(editor._config, editor)
  element.style.whiteSpace = "pre-wrap"

  // ...(textTransformの処理)...

  let result = element

  if (textNode.hasFormat("bold") && !containsTag(element, "strong")) {
    result = wrapWith(result, "b")
  }
  if (textNode.hasFormat("italic") && !containsTag(element, "em")) {
    result = wrapWith(result, "i")
  }
  if (textNode.hasFormat("strikethrough")) {
    result = wrapWith(result, "s")
  }
  if (textNode.hasFormat("underline")) {
    result = wrapWith(result, "u")
  }

  return { element: result }
}

containsTag()ヘルパーは、要素自身のタグ名とquerySelector()による子孫検索の両方で対象タグの存在を確認します。<strong>が既に存在する場合は<b>を追加せず、<em>が既に存在する場合は<i>を追加しません。一方、<s>(strikethrough)と<u>(underline)はcreateDOM()が対応するセマンティクスタグを生成しないため、チェックなしでラップされます。

この関数はsrc/elements/editor.jsのエディタ初期化時に、Lexicalの html.export 設定としてMap形式で登録されます。

html: {
  export: new Map([ [ TextNode, exportTextNodeDOM ] ])
}

テストの変更は変更の広がりを示しています。Playwrightテスト(inline_formatting.test.jsmarkdown.test.jslinks.test.jshighlights.test.js)とCapybaraシステムテスト(trix_html_test.rbcolor_highlighter_test.rbprompts_test.rb)の期待値が一斉に更新されており、いずれも<b><strong>の二重タグから<strong>単体へ修正されています。

設計判断

Lexicalのコアを変更せずに上書きするアプローチが採用されました。Lexicalはhtml.export設定でノード型ごとにexport関数を登録できる拡張ポイントを提供しており、今回はこれを活用してTextNodeの出力処理のみを差し替えています。

関数の実装では、createDOM()を起点とすることで、Lexicalがテーマや設定に応じてDOMを構築するロジックを再利用しています。フォーマット判定の順序(bold→italic→strikethrough→underline)は、<strong>が最内層になるよう設計されており、test/dummy/app/views/sandbox/_default.html.erbで確認できる<i><strong>styles</strong></i>のように、italic(<i>)とbold(<strong>)が共存する場合は意図的に<i>が外側に残ります。これはboldにはセマンティクスタグ<strong>が存在する一方、italicが<em>で既にカバーされていれば<i>を省略し、そうでなければ<i>で囲む、という非対称な処理の結果です。

まとめ

本PRは、Lexicalのhtml.export拡張ポイントを活用することで、ライブラリ本体に手を入れずにHTML出力の品質を修正した変更です。セマンティクスタグと表示用タグの重複チェックという単純なロジックで、広範なテストにわたる冗長なマークアップを一掃しており、出力HTMLをそのまま他システムに渡す際の信頼性が向上します。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
c6810e29

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術的な変更・設計判断(各論)→まとめ(結論)」の3部構成が明確に適用されており、読者が理解しやすい構成になっています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```javascript:ファイルパス)とGitHubのPRリンク記法([#838](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Lexicalライブラリの内部実装に関する内容であり、専門知識を持つエンジニアという対象読者に適した技術レベルと表現が用いられています。

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

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

各セクションが総論→各論の構成になっており、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。これにより高い可読性が確保されています。

Diff内容との照合 ✓ PASS

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

記事で引用されているコードブロックは、提供されたDiffの内容と正確に一致しています。一部の省略も、主要なロジックの理解を妨げない適切な範囲です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「セマンティクスタグ」「表示用タグ」「exportDOM」「createDOM」など、PRの文脈で使われる技術用語が正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

二重タグが発生する原因と、それをカスタム関数でどのように解決するかの技術的な説明が、Diff内のコードロジックと完全に整合しており、正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiff内のコード・テスト修正内容によって裏付けられており、ハルシネーション(捏造)は見られません。

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

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

PR番号(#838)やファイルパス、関数名などの固有名詞はすべて正確に記載されています。

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

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

記事のタイトルはPRのタイトル(Eliminate redundant bold/italic markup in HTML export)の内容を正確に要約しており、記事全体の主題と一致しています。

外部知識の正確性 ✓ PASS

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

PR情報に含まれない外部の知識(バージョンのサポート状況など)の追記はなく、提供された情報に忠実です。

時間表現の正確性 ✓ PASS

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

変更前(問題があった状態)と変更後(解決された状態)の時間的な関係性が正しく記述されており、PRの内容と矛盾しません。