LoginSignup
4
2

More than 3 years have passed since last update.

redcarpetで新しいマークダウン記法をサクッと追加する

Posted at

コロナウィルスにも負けずに相変わらずサウナに行っているプレイライフのエンジニアの合原です。

昨日ちょうど、弊社の特集記事の入稿画面に、redcarpetを用いて、マークダウンでの入力を拡張したので、それについてまとめます。

なぜマークダウン記法の追加をした?

弊社では、従来なかなかの骨のある作りのオリジナルのhtmlエディタが実装されており、
テストもなく、何がデグレするかわからない状態です。
手を加えるとなると、手間も時間もかかることから難しいなと。
何よりも、

  • 「使用者」である編集担当も、使いやすいと思っていない
  • マークダウンも使えるようにして欲しいと言う声が前々からあった

といった点から、redcarpetを用いたマークダウン記法を部分的に追加をすることにしました。
本来であれば、エディタ機能自体リプレースしたいところですが、別案件もあったりと。。。現状厳しく(泣

また一方で、弊社の場合、すでにredcarpetの導入が済んでいたことも対応方法の決定打になりました。

弊社の環境

Rails 5.2.3

まず先に、下記の通り、簡単にredcarpetの導入についてまとめます。
詳細については、こちらもご参考に。

redcarpetの導入

gemインストール

viewから使えるようにヘルパーを追加

app/helpers/markdown_helper.rb

def format_markdown(text)
  renderer = Redcarpet::Render::HTML.new
  markdown = Redcarpet::Markdown.new(renderer, options)
  markdown.render(text).html_safe
end

あとはこれを使えばマークダウンが使えるようになります。

html tag をカスタマイズする方法

ここにある通り。

Redcarpet::Render::HTML.new(options = {})

optionsを使うことでも可能。

example

renderer = Redcarpet::Render::HTML.new(no_links: true, hard_wrap: true)

オリジナルのhtmltag を設定したい場合

ここにある通り。
Redcarpet::Render::HTMLを継承したサブクラスで、
各種renderersメソッドをオーバライドすることで可能。

class CustomRender < Redcarpet::Render::HTML
  def block_quote(quote)
    %(<blockquote class="my-custom-class">#{quote}</blockquote>)
  end
end

独自の記法追加する方法(実際にやったのはこれです

ここにある通り、redcarpetにはコールバックメソッドが用意されています。
postprocessを使えば、filter_htmlオプションの影響を受けず変更が可能となるので、
こちらを下記のようにオーバライドします。

Redcarpet::Render::HTMLのサブクラスを作る

lib/custom_markdown.rb

class CustomMarkdown < Redcarpet::Render::HTML
  :

  def header(title, level)
    "#{'#' * level} #{title}"
  end

  :


  def autolink(link, link_type)
   :
 end

  def codespan(code)
    :
  end

  def postprocess(full_document)
    MarkdownCustoms::ClassNameAppender.new(full_document: full_document).customized_full_document
  end

  :
end

メソッド内では、独自のクラスを追加するマークダウンを解釈して、class属性を付与したHTMLが返します。
あとは、呼び出しているこのクラスのテスト書きます。

テスト(仕様)


require 'rails_helper'

RSpec.describe MarkdownCustoms::ClassNameAppender, type: :model do
  describe '#custom_full_document' do
    let(:class_name_appender) { MarkdownCustoms::ClassNameAppender.new(full_document: full_document) }
    subject { class_name_appender.customized_full_document }
    context 'when full_document == nil' do
      let(:full_document) { nil }
      it { is_expected.to eq full_document }
    end
    context 'when full_document not include class_name ' do
      let(:full_document) { '<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      it { is_expected.to eq full_document }
    end
    context 'when full_document include valid 1 class_name ' do
      let(:full_document) { '[class: button-filled--action]<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      let(:expected_string) { '<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="button-filled--action">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      it { is_expected.to eq expected_string }
    end
    context 'when full_document include valid 2 class_name ' do
      let(:full_document) { '[class: .l-flex--center.button-filled--action]<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      let(:expected_string) { '<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="l-flex--center button-filled--action">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      it { is_expected.to eq expected_string }
    end
    context 'when full_document include valid 1 class_name surrounded single quatation' do
      let(:full_document) { '[ class: &#39;button-filled--action&#39;]<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      let(:expected_string) { '<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="button-filled--action">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      it { is_expected.to eq expected_string }
    end
    context 'when full_document include valid 2 class_name surrounded single quatation' do
      let(:full_document) { '[class: &#39;l-flex--center.button-filled--action&#39;]<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      let(:expected_string) { '<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="l-flex--center button-filled--action">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      it { is_expected.to eq expected_string }
    end
    context 'when full_document include valid 1 class_name surrounded double quatation' do
      let(:full_document) { '[class: &#34;button-filled--action&#34;]<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      let(:expected_string) { '<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="button-filled--action">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      it { is_expected.to eq expected_string }
    end
    context 'when full_document include valid 2 class_name surrounded double quatation' do
      let(:full_document) { '[class: &#34;.l-flex--center.button-filled--action&#34;]<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      let(:expected_string) { '<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="l-flex--center button-filled--action">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      it { is_expected.to eq expected_string }
    end
    context 'when full_document include invalid class_name ' do
      let(:full_document) { '[clss: .button-filled--action]<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      let(:expected_string) { '<a href="https://t.afi-b.com/visit" target="_blank" rel="nofollow noopener" class="">アンカーテキスト</a>' } # rubocop:disable Metrics/LineLength
      it { is_expected.to eq expected_string }
    end
  end
end

以上のようにして、

[class: l-margin--auto.l-flex--center.button-filled--action][アンカーテキスト](アンカーリンク)

と言った記法で、

<a href=アンカーリンク class='l-margin--auto l-flex--center button-filled--action'>
  アンカーテキスト
</a>

となり、redcarpetデフォルトのリンクのマークダウンにクラスを指定する記法を追加できました。

まとめ

きっかけは、上述の通り、レガシーなコードの改修を避けることが大きかったのですが、やってみると意外とスマートにできたかな思います。今後少しづつマークダウンでの本運用も視野に、編集部の方で入れてくれているので、引き続き改良を続けていこうと思います。
近々Rails6のaction textも実際使ってみようかと思うので、こちらもまた、レポートできたらと思います。
ではでは。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2