[Rails]formをMarkdownでの投稿に変更する方法 (helperとmoduleについても解説)
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追加する
完成です😀
こちらが、マークダウン投稿実施時に出会ったセキュリティ問題です
Discussion