Rails / Redcarpet / Rouge / マークダウンで記述するコードブロックの表示をいい感じにする

Shunsuke Sawada

このブログ、マークダウンで書いているのですが、コードの記述が多いので、そこもオシャレにしたいところ。

マークダウンには Redcarpet 、シンタックスハイライトには Rouge を使って独自にカスタマイズしました。

Redcarpet

https://github.com/vmg/redcarpet

Rouge

https://github.com/jneen/rouge

Gemfile
1
2
gem 'rouge'
gem 'redcarpet'

MarkdownをHTMLで出力する

基本的な使い方はこう。
render_optionsextensionsドキュメント を参照してください。

application_helper.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def markdown(text)
  render_options = {
    filter_html: false,
    hard_wrap: true
  }
  renderer = Redcarpet::Render::HTML.new(render_options)

  extensions = {
    autolink: true,
    fenced_code_blocks: true,
    lax_spacing: true,
    no_intra_emphasis: true,
    strikethrough: true,
    superscript: true,
    tables: true,
  }
  Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
end

これをビューで使用する。
@post という記事のオブジェクトがあるとします。

views/posts/show.html.erb
1
<%= markdown(@post.body) %>

Rouge

このままだとコードブロックの見た目がしょぼいので、言語によっていい感じにしたい。
そのために Recarpet の renderer を変更します。
Redcarpet::Render::HTML を継承してカスタムなレンダラークラスを定義する。
その際に Rouge の プラグインをインクルードすれば基本的にはOKです。

custom_markdown_renderer.rb
1
2
3
class CustomMarkdownRenderer < Redcarpet::Render::HTML
  include Rouge::Plugins::Redcarpet
end
application_helper.rb
1
2
3
  # renderer を変更します
  # renderer = Redcarpet::Render::HTML.new(render_options)
  renderer = ::CustomMarkdownRenderer.new(render_options)

さらに Rouge が用意してくれている CSS を読み込みましょう。
Themes の一覧は ココ にあるので、好きなものを選択してください。

_rouge.scss.erb
1
<%= Rouge::Themes::Github.render(:scope => '.highlight') %>

メインのCSSで読み込んで Sprockets にのせる。

application.scss
1
@import 'rouge';

  

さらに自分好みにカスタマイズ

これでもなかなかいい感じに出力してくれるのですが、細かいところはやっぱり自分で何とかしないといけない。欲しかったのは、
  
・行番号の表示
・行番号を span で囲む
・言語名もしくはファイル名を表示
  
という機能。
ファイル名 assets/javascripts/application.js とかが指定されていれば、ファイル名だけ表示、言語名だけが指定されていれば js 等と表示したい。どちらも指定されていなければ何も表示しない、としたい。

カスタムレンダラーの作成方法は Redcarpet の Github に説明が ありますが Rouge との兼ね合いをどうすれば良いのか分からず、しばらく悩みました。

block_code だけでなく、 Rouge::Plugins::Redcarpet によって、追加されている rouge_formatter も上書きします。
もともとの実装は こちら

custom_markdown_renderer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class MarkdownCustomRenderer < Redcarpet::Render::HTML
  include Rouge::Plugins::Redcarpet

  # Override a method from Rouge::Plugins::Redcarpet
  # Add language section above code block.
  def block_code(code, language)

    # --- Extract file name ---------------------- #
    filename = ''
    regx = Regexp.new(/(<!--\s?filename:(\s?.{1,}\s?)-->\n?)/)
    if !(code =~ regx).nil?
      code.match(regx)
      filename = $2.try(:strip) || ''
      code.gsub!(regx, '')
    end
    # -------------------------------------------- #

    lexer = Rouge::Lexer.find_fancy(language, code) || Rouge::Lexers::PlainText
    if lexer.tag == 'make'
      code.gsub! /^    /, "\t"
    end
    formatter = rouge_formatter(lexer)
    result = formatter.format(lexer.lex(code))

    return result if language.blank? && filename.blank?

    # --- Compose language and filename section --- #

    info_inner_html = [filename, language].select(&:present?).map.with_index { |text, i|
      i == 0 ? "<span class='highlight-info__inner'>#{text}</span>" : nil
    }.compact.join('')

    %(<div class='highlight-info'>
        #{info_inner_html}
      </div>
      #{result}
    )
    # -------------------------------------------- #
  end

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

  

デザイン

最後にスタイルをあてれば完成です。
なんだかんだで結構見た目の調整はしちゃいました。
先程の SCSS ファイルにスタイルを追加します。

_rouge.scss.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<%= Rouge::Themes::Github.render(:scope => '.highlight') %>

// Custome style for rouge formatter
// formatter is defined in application_helper.rb
.highlight {
    margin-top: 10px;
    margin-bottom: 40px;
    pre {
      overflow: auto;
      word-wrap: normal;
      white-space: pre;
      font-size: 0.9em;
      line-height: 1.5;
      border-radius: 2px;
      padding: 0;
        margin: 0;
        code {
            font-size: 1em;
            color: #466568;
            table.rouge-table {
                margin: 0;
                // code
                td.rouge-code {
                    padding: 0;
                    vertical-align: top;
                    pre {
                        padding: 5px 10px;
                        line-height: 1.6em;
                    }
                }
                // line number
                td.rouge-gutter {
                    text-align: center;
                    background-color: #ddd;
                    padding: 5px;
                    width: 20px;
                    vertical-align: top;
                    pre {
                        line-height: 1.6em;
                    }
                }
            }
        }
    }
}

.highlight-info {
    margin-top: 10px;
    margin-bottom: 2px;
    .highlight-info__inner {
        display: inline-block;
        border-left: 1px solid #ddd;
        padding: 1px 7px 2px;
        font-size: 0.8em;
        letter-spacing: 0.5px;
    }
}

.highlight-info + .highlight {
    margin-top: 0;
}

// Override rouge theme
.highlight {
    .c1 {
        color: #939393;
    }
    .s1 {
        color: #bf4c00;
    }
}

  

使い方

Screen_Shot_2017-06-15_at_20.29.04

言語名を表示 + シンタックスハイライト

ファイル名を表示 + シンタックスハイライト


せっかくマークダウンなのに HTML のコメント方式で書いてるのはイケてない感。
が、力尽きたのでひとまずこのままで。

以上です!
ブログ書くのにもテンションが必要なので、
見た目がキレイって大事だなと思った次第です。

3
Shunsuke Sawada

おすすめの記事

acts-as-taggable-on タグを表示させる順番を決めたい
Railsを4.2にバージョンアップしたら、Vagrantのローカル開発環境にアクセスできなくなった問題
Railsのバリデーションエラー後にレイアウトが崩れるとき