Action TextのMarkdown変換で通常テキストをエスケープ処理

rails/rails

Action Textの to_markdown メソッドが、Markdownのメタ文字を含む通常テキストを適切にエスケープするようになりました。これにより、リッチテキストコンテンツ内の #* などの文字が意図せずMarkdown構文として解釈される問題が解消されます。

背景

#56858 でAction TextにMarkdown出力機能が追加されましたが、HTMLテキストノード内のMarkdownメタ文字がエスケープされていませんでした。例えば <p>## Look at *this*</p> というリッチテキストが見出しとして解釈されてしまう問題がありました。

この問題は、通常のテキストコンテンツとMarkdown構文の区別ができないことに起因します。ユーザーが入力した ##* は文字通りの意味で使われることが多く、Markdown構文として扱われるべきではありません。

技術的な変更

HTMLテキストノード内のCommonMarkメタ文字をバックスラッシュでエスケープする処理が追加されました。

エスケープ対象の文字パターン

actiontext/lib/action_text/markdown_conversion.rbMARKDOWN_METACHARACTERS 正規表現が追加され、以下のパターンがエスケープされます:

MARKDOWN_METACHARACTERS = /
  [\\`*_{}\[\]|~<>]     # 一般的にエスケープすべきメタ文字
  | \A\#(?=[\s\#]|\z)   # 行頭のハッシュ(ATX見出し)
  | \A=(?=[=\s]|\z)     # 行頭のイコール(Setext見出し)
  | \A-                 # 行頭のハイフン(リスト項目、区切り線、Setext見出し)
  | \A\+(?=\s|\z)       # 行頭のプラス(リスト項目)
  | \A\d+\K\.(?=\s|\z)  # 行頭の「1.」(順序付きリスト項目)
/x

この正規表現は、文脈に応じてエスケープの必要性を判断します。例えば # は行頭かつ後続がスペースまたはハッシュの場合のみエスケープされます。

エスケープメソッドの公開API化

escape_markdown_text メソッドが private から public に変更され、アプリケーション側で独自のAttachableを実装する際に利用できるようになりました:

def escape_markdown_text(text)
  text.gsub(MARKDOWN_METACHARACTERS) { |c| "\\#{c}" }
end

Attachment表現の保護

actiontext/lib/action_text/content.rbto_markdown メソッドが変更され、Attachmentのマークダウン表現を <action-text-markdown> 要素でラップするようになりました:

def to_markdown
  render_attachments(with_full_attributes: false) { |attachment|
    ActionText::HtmlConversion.create_element("action-text-markdown").tap do |node|
      node.content = attachment.to_markdown
    end
  }.fragment.to_markdown
end

SKIP_ESCAPING_PARENTS 定数に action-text-markdown が追加され、この要素内のコンテンツはエスケープ処理をスキップします。これにより、Attachableの attachable_markdown_representation が生成したMarkdown構文は保護されます。

Attachmentのキャプションとファイル名のエスケープ

actiontext/lib/action_text/engine.rbActiveStorage::BlobRemoteImage の表現でもエスケープが適用されます:

def attachable_markdown_representation(caption = nil)
  "[#{MarkdownConversion.escape_markdown_text((caption || filename).to_s)}]"
end

キャプションやファイル名に含まれる [] などのメタ文字がエスケープされ、Markdownリンク構文が壊れることを防ぎます。

設計判断

特定の親要素配下ではエスケープをスキップする方式 が採用されました。

SKIP_ESCAPING_PARENTSINLINE_ELEMENTS の2つの定数が追加され、<code><pre> 要素内ではエスケープが行われません。これは、コードブロック内の文字は既にリテラルとして扱われるMarkdown構文の性質に合わせた判断です。

また、PR本文で「このPRは必要のない箇所でもエスケープしている可能性がある」と明記されています。現時点では安全側に倒してエスケープを行い、将来的に正規表現を複雑化させて精緻化する余地を残す方針が取られました。過剰なエスケープは可読性をわずかに下げますが、Markdown構文の誤解釈による機能的な問題を防ぐことを優先した実装です。

まとめ

本PRは、Action TextのMarkdown出力機能において、通常テキストとMarkdown構文を明確に区別する仕組みを導入しました。テキストノードのメタ文字エスケープと、Attachment表現の保護用要素の追加という2つのアプローチにより、ユーザー入力テキストが意図しない形式で出力される問題を解消しています。escape_markdown_text の公開により、独自Attachableの実装者も同じエスケープロジックを利用できます。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の構成が記事全体で明確に適用されています。必須要素はすべて含まれており、理想的な構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```ruby:path/to/file.rb)とGitHubのPR番号リンク([#123](URL))のカスタム構文が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Action TextやMarkdownに関する技術的な詳細を扱っており、専門知識を持つエンジニアという対象読者に適合した内容です。初心者向けの過度な説明はありません。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切で、非常に読みやすい構造です。

Diff内容との照合 ✓ PASS

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

記事内で引用されているすべてのコードブロックは、提供されたDiff情報と完全に一致しています。コードの改変や省略はなく、ファイル名も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「メタ文字」「エスケープ」「CommonMark」「Attachable」など、使用されている技術用語はPRの文脈と一般的に照らして正確かつ適切です。

説明の技術的正確性 ✓ PASS

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

変更の目的、具体的な実装(正規表現、ラッパー要素の追加など)、影響範囲に関する説明はすべて技術的に正確であり、PR情報とDiffによって裏付けられています。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiffの内容に基づいています。特に「設計判断」セクションで言及されている過剰エスケープの可能性については、PR本文の記述を正確に反映しており、ハルシネーションは見られません。

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

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

PR番号(#56858, #56873)、定数名(MARKDOWN_METACHARACTERS)、ファイルパスなど、記事に含まれる固有名詞や数値はすべて正確です。

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

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

記事のタイトル「Action TextのMarkdown変換で通常テキストをエスケープ処理」は、PRの主題である「Escape markdown metacharacters in text nodes」を的確に要約しており、内容と一致しています。

外部知識の正確性 ✓ PASS

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

記事には、PR情報に記載のないバージョン情報やリリース日程などの外部知識は含まれていません。すべての情報が提供された資料に基づいています。

時間表現の正確性 ✓ PASS

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

「〜ようになりました」「〜が追加されました」といった時間表現は、PRによって完了した変更を正しく反映しており、PR情報との矛盾はありません。