Action Text の `to_markdown` における Trix HTML 変換精度の向上
Action Text の ActionText::MarkdownConversion が Trix 生成 HTML をより正確にMarkdown変換できるよう、3つの改善が加えられました。<del> タグのサポート追加、<div> ブロックの透過処理、そしてプリティプリント用ホワイトスペースの除去です。
背景
Trix エディタが出力する HTML には、汎用的な HTML パーサーが想定しない3つのクセがあり、to_markdown の変換結果に乱れが生じていました。
第一に、Trix は打ち消し線に <s> ではなく <del> を使います。これは既存の visit_s ハンドラでは処理されなかったため、打ち消し線が変換結果に反映されませんでした。第二に、Trix はデフォルトのブロック要素として <p> ではなく <div> を使用し、改行を <br> タグで表現します(Trix ソースの piece_view.js / block_view.js の実装がこの規約に基づいています)。<div> に対するハンドラが存在しなかったため、ブロックコンテンツが意図したとおりに変換されませんでした。第三に、シリアライザやテンプレートがプリティプリント済みの HTML を生成する場合、テキストノード間のインデント用ホワイトスペースが Markdown 出力に漏れ出す問題がありました。
技術的な変更
この PR では actiontext/lib/action_text/markdown_conversion.rb に対して3か所の変更が加えられています。
① <del> のサポート追加
<del> を INLINE_ELEMENTS リストに追加したうえで、visit_s への alias として visit_del を定義しています。
# 変更前
INLINE_ELEMENTS = %w[
action-text-markdown
a abbr b bdi bdo cite code data dfn em i kbd mark q
rp rt ruby s samp small span strong sub sup time u var
].freeze
# 変更後
INLINE_ELEMENTS = %w[
action-text-markdown
a abbr b bdi bdo cite code data del dfn em i kbd mark q
rp rt ruby s samp small span strong sub sup time u var
].freeze
alias_method :visit_del, :visit_s
これにより <del>hello</del> が ~~hello~~ に変換されます。
② visit_div ハンドラの追加
Trix が <div> をブロック要素として使い、改行を <br> で表現する規約に合わせ、visit_div は <p> と異なり末尾に \n\n を付加しません。
# Trix uses <div> as its default block element and represents newlines as <br> tags
# (see piece_view.js and block_view.js in the Trix source). Unlike <p>, we don't append
# paragraph-separating newlines here because the <br> children already provide spacing.
def visit_div(_node, child_values)
join_children(child_values)
end
既存の visit_p は "#{join_children(child_values)}\n\n" を返すのに対し、visit_div は <br> による改行をそのまま活かすため余分な改行を加えません。
③ プリティプリント用ホワイトスペースの除去
2つの正規表現定数を追加し、テキストノードの前後に付くインデント用ホワイトスペース(改行+空白の組み合わせ)を除去する strip_pretty_print_indentation を導入しています。
LEADING_PRETTY_PRINT_WHITESPACE = /\A\s*\n\s*/
TRAILING_PRETTY_PRINT_WHITESPACE = /\s*\n\s*\z/
テキストノードの処理は escape_markdown_text(node.content) から escape_markdown_text(strip_pretty_print_indentation(node)) に変更されました。ただし、インラインの兄弟要素に隣接するテキストノード境界では、単語の区切りを保つために空白は完全除去ではなく1スペースに縮退します。これにより **before** ~~middle~~ *after* のような変換が正しく行われます。
設計判断
visit_div が visit_p と別実装になっている点 が設計上の核心です。両者は「ブロック要素のコンテンツをMarkdownに変換する」という目的を共有しますが、<p> はそれ自身が段落の区切りを意味するのに対し、Trix の <div> + <br> の組み合わせでは <br> が改行の責務を担います。末尾改行の付加責務を <div> から切り離すことで、Trix の規約を既存の <br> 変換ロジックを変えずに収容しています。
またプリティプリントホワイトスペースの除去は、インライン要素境界での完全除去ではなく「1スペースへの縮退」という方針を採用しています。これは Markdown トークンの融合(**bold**~~struck~~ のように空白なしで並んでしまう問題)を防ぎつつ、to_markdown の出力が入力 HTML のインデントスタイルに依存しないようにするための判断です。
まとめ
本PRは Trix の3つの出力特性(<del> の使用・<div> ベースのブロック・プリティプリントHTML)に対して、それぞれ最小限のハンドラと正規表現で対処した変更です。変換ロジックの核心部には手を加えず、Trix 固有の規約を既存フレームワークの自然な拡張として取り込んだ設計は、将来的に他のエディタ固有の HTML 規約にも同様のアプローチが取れることを示しています。