👻

[Rails]formをMarkdownでの投稿に変更する方法 (helperとmoduleについても解説)

2023/04/24に公開

Markdownでの投稿可能にする

技術投稿サイトをRailsで作成した際に、投稿をマークダウンに対応させました!

このようにコードブロックを表示できたり、そのコードをコピー可能にしたり、

このようにtableができたり...

一通りのマークダウンに対応してくれるように実装していきます!

今回の前提条件:投稿は可能な状態のこと

必要なgem:

今回使用するgemは2つ!!!
Markdownで書いた文章をHTMLに変換してくれる"Redcarpet"と、

コードのシンタックスハイライトを行ってくれる"Rouge"です

  • redcarpet: markdown parser
    • Rubyのマークダウンパーサーライブラリ
    • マークダウンを使用してドキュメントを書くことができるようにするもの。
  • rougy: code syntax hjighlighter
    • コードのシンタックスハイライトを行ってくれるgem

※parserとは:

  • ソースコードファイルを解析するコンパイラまたはインタプリタのモジュール。
  • より一般的に言えば、テキストを解析して内容を別の表現に変換するソフトウェア
  • パーサーライブラリは、パーサーを実装するためのライブラリ
  • 参照:MDN: Parser (パーサー)について

実装していく!!!

1. gemのインストール

gem "redcarpet"
gem 'rouge'
  • bundle installを忘れずに。

2. Redcarpet, roungeの設定

2-1. Redcarpetの設定

  • app/helpers/配下にmarkdown_helper.rbを作成し、以下のように記述。
# frozen_string_literal: true

require 'rouge/plugins/redcarpet'
require 'redcarpet'
require 'redcarpet/render_strip'

class CustomRenderHTML < Redcarpet::Render::HTML
  include Rouge::Plugins::Redcarpet

  # Rouge::Plugins::Redcarpetのメソッドを上書きする
  def block_code(code, language)
    # もしコードブロックに言語とファイル名が定義されたら取得する。例: ```ruby:test.rb
    filename = ''
    if language.present?
      filename = language.split(':')[1]
      language = language.split(':')[0]
    end

    lexer = Rouge::Lexer.find_fancy(language, code) || Rouge::Lexers::PlainText
    code.gsub!(/^    /, "\t") if lexer.tag == 'make'
    formatter = rouge_formatter(lexer)
    result = formatter.format(lexer.lex(code))
    return "<div class=#{wrap_class}>#{copy_button}#{result}</div" if filename.blank? && language.blank?

    compose_filename_and_language(result, filename, language)
  end

  def rouge_formatter(_options = {})
    options = {
      css_class: 'highlight',
      line_numbers: true,
      line_format: '<span>%i</span>'
    }
    Rouge::Formatters::HTMLLegacy.new(options)
  end

  private

  # wrap CSSクラス名の定義
  def wrap_class
    'highlight-wrap'
  end

  # コピーボタンの定義。クリックするとJavaScriptファンクションが実行される
  def copy_button
    "<button onclick='copy(this)'>Copy</button>"
  end

  # コードブロックの言語、ファイル名、コピーボタンを設置する
  def compose_filename_and_language(result, filename, language)
    info_section = [filename, language].select(&:present?).map.with_index do |text, i|
      i.zero? ? "<span class='highlight-info'>#{text}</span>" : nil
    end.compact.join

    %(<div class=#{wrap_class}>
        #{copy_button}
        #{info_section}
        #{result}
      </div>
    )
  end
end

module MarkdownHelper
  def plaintext(text)
    markdown = Redcarpet::Markdown.new(Redcarpet::Render::StripDown)
    markdown.render(text)
  end
  def markdown(text)
    options = {
      with_toc_data: true,
      hard_wrap: true
    }
    extensions = {
      no_intra_emphasis: true,
      tables: true,
      fenced_code_blocks: true,
      autolink: true,
      lax_spacing: true,
      lax_html_blocks: true,
      footnotes: true,
      space_after_headers: true,
      strikethrough: true,
      underline: true,
      highlight: true,
      quote: true
    }

    renderer = CustomRenderHTML.new(options)
    markdown = Redcarpet::Markdown.new(renderer, extensions)
    markdown.render(text).html_safe
  end

  def toc(text)
    renderer = Redcarpet::Render::HTML_TOC.new(nesting_level: 6)
    markdown = Redcarpet::Markdown.new(renderer)
    markdown.render(text).html_safe
  end
end
helperファイル

Ruby on RailsのようなWebアプリケーションフレームワークにおいて、
複数のビューで共通のメソッドや定数を定義するために利用される
( Ruby on RailsのようなWebアプリケーションフレームワークで利用される、ビューで利用するためのメソッドや定数を定義するためのファイル。)
一般的に、helperファイル内でmoduleを定義し、ビューで利用するメソッドを定義することが多い。

moduleとmoduleメソッドについて

module

  • Rubyにおけるmoduleとは、
    オブジェクト指向プログラミングにおける機能の一つで、
    複数のクラス間で共通のメソッドや定数を定義するための仕組み。
  • クラスと同様に、moduleもオブジェクト指向プログラミングの重要な概念の一つ。

moduleメソッド

  • moduleに対して定義することのできる特別なメソッドのこと。
    moduleメソッドを定義することで、そのmoduleが提供する機能を柔軟にカスタマイズしたり、拡張したりすることができる。
    代表的なmoduleメソッドには、以下のようなものがある。

include

includeは、moduleをクラスの中に取り込むためのメソッド。
includeされたmoduleのインスタンスメソッドが、クラスのインスタンスメソッドとして利用可能になります。

module Greeting
  def say_hello
    puts "Hello!"
  end
end
    
class Person
  include Greeting
end
    
person = Person.new
person.say_hello #=> "Hello!"

extend

extendは、moduleをオブジェクトに取り込むためのメソッド。
extendされたmoduleのメソッドが、オブジェクトの特異メソッドとして利用可能になる。

module Greeting
  def say_hello
    puts "Hello!"
  end
end
    
class Person
end
    
person = Person.new
person.extend(Greeting)
person.say_hello #=> "Hello!"

まだありますが、これらのmoduleメソッドを適切に使うことで、
Rubyにおけるオブジェクト指向プログラミングの柔軟性を高めることができる。

def markdownの間の記述について

  • optionsのパラメーター
パラメーター 意味
with_toc_data 見出しにアンカーを付ける
hard_wrap 改行を
タグに変換
  • extensionsのパラメーターと意味
パラメーター 意味
no_intra_emphasis 単語内の強調を解析しない
tables テーブルを有効化
fenced_code_blocks 複数行のコードを有効化
autolink http https ftpで始まる文字列を自動リンク
lax_spacing 複数行のコードの前後に空行が不要
space_after_headers 見出しは#の後にスペースを空ける

renderer = CustomRenderHTML.new(options)

  • Rougeによるシンタックスハイライトを有効にするため、Rougeのプラグインをincludeした
    カスタムクラスのインスタンスとしてレンダラーを作成。

tocメソッド

TOCとはTable Of Contentsのことで、目次を意味。
markdownメソッド内でwith_toc_dataを有効にしている必要があります。

toc_optionのパラメーターとその意味は以下の通り。

パラメーター 意味
nesting_level 見出しのネストレベル(整数)

▷ 表示方法

以下のようにヘルパーを呼び出し変更することで、Markdownが表示されます。

<%= toc(@article.content) %>
<%= markdown(@article.content) %>

2-2. rougeの設定を行う

app/assets/stylesheets/配下に_rouge.scss.erbを作成、rougeのカラーテーマを読み込む。
app/assets/stylesheets/_rouge.scss.erb

<%= Rouge::Themes::Base16.render(:scope => '.highlight') %>

最後に、
assets/stylesheetsapplication.scssに以下を追記してrougeとmarkdownをインポート。
assets/stylesheetsapplication.scss

@import 'rouge';
@import 'markdown';

3. コードブロックのコードのコピーを可能にする

helperの中に以下のような記述がある。

# コピーボタンの定義。クリックするとJavaScriptファンクションが実行される
  def copy_button
    "<button onclick='copy(this)'>Copy</button>"
  end

このJavaScriptを定義していく!!!!

window.copy = function(e) {
    // クリックしたボタンに紐づくコードの範囲の定義
    let code = e.closest('.highlight-wrap').querySelector('.rouge-code')

    // クリップボードにコードをコピーしてから、ボタンのテキストを変更する
    navigator.clipboard.writeText(code.innerText)
      .then(() => e.innerText = 'Copied')

    // 任意:コピーしたコードが選択されたようにする
    window.getSelection().selectAllChildren(code)
  }

4. 投稿formを変更する

以下のように変更していきます。

# <%= post.body %>
# 上記を以下のように変更
<%= markdown(post.body) %>

5. 好みにCSS追加する


完成です😀

こちらが、マークダウン投稿実施時に出会ったセキュリティ問題です

https://zenn.dev/airiswim/articles/3c1f9ee5d012b1

Discussion