コロナウィルスにも負けずに相変わらずサウナに行っているプレイライフのエンジニアの合原です。
昨日ちょうど、弊社の特集記事の入稿画面に、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: '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 surrounded single quatation' 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 double quatation' 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 surrounded double quatation' 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 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も実際使ってみようかと思うので、こちらもまた、レポートできたらと思います。
ではでは。