Action TextのMarkdown変換で通常テキストをエスケープ処理
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.rb に MARKDOWN_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.rb の to_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.rb の ActiveStorage::Blob と RemoteImage の表現でもエスケープが適用されます:
def attachable_markdown_representation(caption = nil)
"[#{MarkdownConversion.escape_markdown_text((caption || filename).to_s)}]"
end
キャプションやファイル名に含まれる [ や ] などのメタ文字がエスケープされ、Markdownリンク構文が壊れることを防ぎます。
設計判断
特定の親要素配下ではエスケープをスキップする方式 が採用されました。
SKIP_ESCAPING_PARENTS と INLINE_ELEMENTS の2つの定数が追加され、<code> や <pre> 要素内ではエスケープが行われません。これは、コードブロック内の文字は既にリテラルとして扱われるMarkdown構文の性質に合わせた判断です。
また、PR本文で「このPRは必要のない箇所でもエスケープしている可能性がある」と明記されています。現時点では安全側に倒してエスケープを行い、将来的に正規表現を複雑化させて精緻化する余地を残す方針が取られました。過剰なエスケープは可読性をわずかに下げますが、Markdown構文の誤解釈による機能的な問題を防ぐことを優先した実装です。
まとめ
本PRは、Action TextのMarkdown出力機能において、通常テキストとMarkdown構文を明確に区別する仕組みを導入しました。テキストノードのメタ文字エスケープと、Attachment表現の保護用要素の追加という2つのアプローチにより、ユーザー入力テキストが意図しない形式で出力される問題を解消しています。escape_markdown_text の公開により、独自Attachableの実装者も同じエスケープロジックを利用できます。