🦓

[Rails]redcarpetとrougeによるマークダウン機能

2023/07/28に公開

はじめに

Redcarpetを使って投稿にMarkdownで書いた文章をHTMLに変換していきます。

Redcarpetは、Markdown形式のテキストをHTMLに変換するためのgemです。

Redcarpet::Render::HTMLを使用してHTMLレンダラーを作成してくれます。
require 'redcarpet'

# Markdownテキストを定義
markdown_text = "# Hello, Redcarpet!\n\nThis is **bold** and this is *italic*."

# Redcarpetのインスタンスを作成
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML)

# HTMLに変換
html_output = markdown.render(markdown_text)

# HTMLを表示
puts html_output

マークダウンテキストの出力結果は次のようになります:

<h1>Hello, Redcarpet!</h1>

<p>This is <strong>bold</strong> and this is <em>italic</em>.</p>

html以外にも、Markdownを他の言語のフォーマットに変換することもできます。
https://github.com/vmg/redcarpet

環境

Rails 7.0.4
ruby 3.2.2

Redcarpetをインストールする:

Gemfile
gem 'redcarpet'
bundle install

Loofah SafeListを追加する

Loofah SafeListは、RubyのgemであるLoofahを使用してHTMLをサニタイズ(安全にする)する際に、特定の要素や属性を許可するためのリストです。

サニタイズは、不正なコードや悪意のあるスクリプトが埋め込まれるのを防ぎ、安全なHTMLを生成するための重要な手段です。

Loofah SafeListを追加することでマークダウンをHTMLに変換する場合HTMLの安全化を行うことができます。

require 'loofah'

# サニタイズするHTML
html = <<-HTML
  <h1>Title</h1>
  <p>Paragraph <span style="color: red;">with red color</span></p>
  <script>alert('This is a malicious script!');</script>
HTML

# safelistを定義
safelist = Loofah::HTML5::SafeList.new
safelist.allow_elements("h1", "p", "span") # h1, p, span要素を許可
safelist.allow_attributes("style") # style属性を許可

# HTMLをサニタイズ
sanitized_html = Loofah.fragment(html).scrub!(safelist).to_s

puts sanitized_html

上記の例では、<h1><p>、および<span>要素を許可し、style属性を許可するSafeListを定義しています。<script>要素は許可されていないので、サニタイズされたHTMLから削除されます。

出力結果は次のようになります:

<h1>Title</h1>
<p>Paragraph <span style="color: red;">with red color</span></p>

<script>要素が完全に削除されていることが分かります。Loofah SafeListを使用することで、指定した要素と属性以外のHTMLが安全に削除され、安全なコンテンツのみが残されます。
これにより、悪意のあるコードが埋め込まれるのを防ぎ、セキュリティを向上させることができます。

Loofahのファイルを使ってアプリにSafeListを追加していきます。
lib/loofah/html5/safelist.rbファイルは、Loofahgemの一部であり、HTMLのサニタイズ(安全化)のために使用されるSafeListクラスが定義されているファイルです。

SafeListクラスは、許可されたHTML要素や属性のリストを管理します。これにより、HTMLのサニタイズ処理において、指定した要素と属性以外の部分を削除することが可能となります。
https://github.com/flavorjones/loofah/blob/main/lib/loofah/html5/safelist.rb

safelist.rbを作成する

アプリにlib/loofah/html5/safelist.rbを作成し、Loofahのコードをペーストします。

application.rbに読み込む

作成したsafelist.rbを読み込みます。

config/application.rb
class Application < Rails::Application
...
    config.action_view.sanitized_allowed_tags = Loofah::HTML5::SafeList::ALLOWED_ELEMENTS
    config.action_view.sanitized_allowed_attributes = Loofah::HTML5::SafeList::ALLOWED_ATTRIBUTES
end

マークダウンヘルパーを作成する

markdownオブジェクトにマークダウさせたいテキストと拡張機能の二つの引数を渡す必要があります。

# Initializes a Markdown parser
markdown = Redcarpet::Markdown.new(renderer, extensions = {})

Redcarpetでサポートされている主な拡張機能:

  1. :tablesを有効にすることで、テーブルを使用できるようになります。

  2. :tasklistを有効にすることで、タスクリストを使用できるようになります。

  3. :strikethroughを有効にすることで、取り消し線のような取り消し線を使用できるようになります。

  4. :autolinkを有効にすることで、URLやメールアドレスなどの自動リンクが有効になります。

  5. :with_toc_data拡張を有効にすることで、マークダウンに目次(Table of Contents)を作成する際に、各セクションに対して自動的にIDを追加します。

  6. :no_intra_emphasis : 通常、_で囲まれたテキスト(イタリック)の中に別の_がある場合、それをイタリックとして解釈します。しかし、no_intra_emphasisを有効にすると、中の_はイタリックとして解釈せず、テキスト内でアンダースコアを使用できるようになります。

  7. :fenced_code_blocks: バッククォート3つ(```/~~~)で囲むことでコードブロックを表現できるようにします。

  8. :disabled_indented_code_blocks: インデントによるコードブロック(4つのスペースで開始される行)を無効にします。

  9. :lax_spacing: マークダウンのパース時に、ヘッダーやリストなどのブロック要素の前後に厳密な空白がなくても解釈します。

  10. :space_after_headers: #ヘッダーの後に空白を必要とするかどうかを設定します。例えば、# Headerはヘッダーとして解釈されますが、#Headerはヘッダーとして解釈されません。

  11. :superscript: ^テキスト^のような上付き文字を有効にします。

  12. :underline: <u>テキスト</u>のような下線を有効にします。

  13. :highlight: ==テキスト==のようなハイライトを有効にします。

  14. :quote: > テキストのような引用を有効にします。

  15. :footnotes: マークダウン内で脚注を使用することを有効にします。

https://markdown-it.github.io/

redcarpetがマークダウン以外に、目次を作成してくれるRedcarpet::Render::HTML_TOCレンダラーも用意してます。
toctable of contentsの最初の文字を取って作られた略称です。

app/helper/markdown_helper.rb
module MarkdownHelper
    def markdown(text)
        options = {
            hard_wrap: true,
            link_attributes: { rel: 'nofollow', taget: '_blank' },
            fenced_code_blocks: true,
            no_intra_emphasis: true,
            autolink: true,
            tables: true,
            lax_spacing: true,
            underline: true,
            highlight: true,
            quote: true, 
            footnotes: true
        }
        markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, options)
        sanitize(markdown.render(text))
    end
    
    def toc(text)
        renderer = Redcarpet::Render::HTML_TOC.new(nesting_level: 3)
        markdown = Redcarpet::Markdown.new(renderer)
        markdown.render(text).html_safe
    end
end

マークダウンヘルパーを使う

markdown()ヘルパーを使うことで投稿の本文をマークダウンからHTMLに変換して表示しています。これにより、マークダウン形式のテキストがHTMLとして正しく表示されるようになります。

app/views/articles/show.html.erb
<h1><%= article.title %></h1>

<%= toc(article_body) %>
<%= markdown(article.body) %>

早速試してみます、投稿の本文にマークダウンテキストを入れてみます。
htmlに変換されましたね。

本文の上に目次も作成されました。

Fakerを使ってダミーマークダウンを作成する

fakerがマークダウンにも対応しているため使っていきます。

db/seeds.rb
5.times do |index|
    Article.create!(
      user: User.offset(rand(User.count)).first,
      title: "投稿#{index}",
      body: Faker::Markdown.sandwich(sentences: 6, repeat: 3)
    )
  end
bin/rails db:seed

https://github.com/faker-ruby/faker/blob/main/doc/default/markdown.md

投稿にマークダウンを使えるようになりました。
次ではrougeを使ってコードのシンタックスハイライトを入れていきます。
rougeはシンタックスハイライターgemであり、コードブロックなどのソースコードをカラフルにハイライト表示するために使用されます。

https://github.com/rouge-ruby/rouge

rougeをインストールする

Gemfile
gem 'rouge'
bundle install

rougeのカスタムレンダラーを作成する

app/markdown_helper.rb
module MarkdownHelper
    require 'rouge'
    require 'rouge/plugins/redcarpet'

    class RougeHTML < Redcarpet::Render::HTML
        include Rouge::Plugins::Redcarpet
    end

    def markdown(text)
        options = {
            hard_wrap: true,
            link_attributes: { rel: 'nofollow', taget: '_blank' },
            fenced_code_blocks: true,
            no_intra_emphasis: true,
            autolink: true,
            tables: true,
            lax_spacing: true,
            underline: true,
            highlight: true,
            quote: true, 
            footnotes: true
        }
-       markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, options)
+       markdown = Redcarpet::Markdown.new(RougeHTML, options)
        sanitize(markdown.render(text))
    end
end

https://github.com/rouge-ruby/rouge/blob/master/lib/rouge/plugins/redcarpet.rb

rouge.scss.erbを作る

rougeのカラーテーマを使うためそれを読み込むスタイルシートファイルを作成します。

app/stylesheets/rouge.scss.erb
<%= Rouge::Themes::Thankful_eyes.render(:scope => '.highlight') %>

.scss.erb

.scss.erbファイルは、SCSSとERBの両方を組み合わせたファイルです。

.scss.erbファイルは、これらの2つの拡張子を組み合わせたもので、SCSSとERBの機能を組み合わせて利用することができます。このファイル形式は、例えば以下のような場面で使用されます:

  1. 動的なスタイルシートの生成: ERBを使って動的な値を計算し、SCSS内の変数に埋め込んでスタイルシートを生成することができます。

  2. 条件付きスタイルの設定: ERBを使って条件文を記述し、条件に応じて異なるスタイルを適用することができます。

  3. ローカライズ: ERBを使って多言語サポートを実現する際に、言語ごとに異なるスタイルを適用することができます。

// app/assets/stylesheets/custom.scss.erb

$primary-color: <%= current_user.theme_color %>; // ERBで動的な値を取得

body {
  background-color: $primary-color;
}

<% if current_user.admin? %>
.admin-link {
  color: red;
}
<% else %>
.admin-link {
  color: blue;
}
<% end %>

この例では、current_user.theme_colorによってユーザーごとに異なるテーマカラーを取得し、スタイルシートに反映しています。また、current_user.admin?によってユーザーが管理者かどうかを判断し、条件に応じて異なるスタイルを適用しています。

rougeのカラーテーマ:

https://github.com/rouge-ruby/rouge/tree/master/lib/rouge/themes

こちらからカラーテーマをプレビューすることができます:
https://spsarolkar.github.io/rouge-theme-preview/

コードをハイライトされる前:

ハイライトされたことを確認します:

終わり

投稿にマークダウンテキストを書けるようになりましたね。
シンタックスハイライトを導入することでコードをかなり読みやすくなりましたね。

https://blog.corsego.com/markdown-styling-with-rouge

Discussion