Narito Blog

Django、Markdownエディタの導入(django-markdownx)

- PythonDjangoDjangoライブラリMarkdown

概要

django-markdownxを使い、DjangoプロジェクトにMarkdownエディタを導入していくサンプルです。

Markdown記法とは

次の文章は、Markdownで書かれています。

# Markdown記法とは
文書を記述するための軽量マークアップ言語の一つで、プレーンテキスト形式で手軽に書いた文書からHTMLを生成するために開発されました。

## Markdownの特徴
次のような特徴があります。

* HTML文書に変換されたりする
* Markdownで書かれたファイルは、`.md`という拡張子になる
* 記法のルールがシンプルで、Markdownそのままでも割と読みやすい

## 利用例
エンジニアが使うサービスでは、文章をMarkdownで書けたり、Markdownを強制してくるものも多くあります。

1. Qiitaの投稿、コメント欄
1. Teratailの質問、回答欄
1. Stack Overflowの質問や回答欄
1. GithubやBitbucketのIssueやコメント欄、リードミー

上の文章がどういうHTMLに変換されるか、何となく察しがつくと思います。#h1要素になり、##h2*はそれぞれ箇条書きのリストになり、1.は番号付きのリストです。

HTMLに変換することができ、そのままでも割と見やすいというのが特徴です。結構いろんなサービスがMarkdownに対応しており、エンジニアならMarkdownくらい書こう的な圧力も感じる昨今です。

私のブログの記事も、今はMarkdownで管理しています。もちろん今書いているこの記事もです。
この記事のテキスト

最近、PyPIにアップロードするライブラリの説明もMarkdownで書けるようになりました。

django-markdownx

このMarkdownを、Djangoで簡単に扱うためのライブラリが幾つかあります。その一つがdjango-markdownxです。

Markdown記法で書いてそれを表示するだけならば、models.TextFieldなフィールドにMarkdownで記事の内容を書いて、テンプレートではMarkdown評価用ライブラリを使ってHTMLとして出力させることもできます。

django-markdownxのようなライブラリは、リアルタイムプレビュー機能ドラッグ&ドロップでの画像添付機能admin管理サイトもサポート、といった機能が追加されており、なかなかに便利です。私も使っています。

まず、インストールします。

pip install django-markdownx

settings.pyINSTALLED_APPSに追加します。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app.apps.AppConfig',  # マイアプリケーション
    'markdownx',  # これ
]

プロジェクトのurls.pyにも足しまして...

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app.urls')),  # マイアプリ
    path('markdownx/', include('markdownx.urls')),  # これ
]

以下のようなモデルを例に説明していきます。Markdownで書きたいフィールドに対して、markdownx.models.MarkdownxFieldを使います。

from django.db import models
from markdownx.models import MarkdownxField


class Post(models.Model):
    text = MarkdownxField('本文', help_text='Markdown形式で書いてください。')

ファイルアップロードをする際は、MEDIAファイルの設定もしておきましょう。settings.py

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

プロジェクトのurls.py

from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app.urls')),  # マイアプリ
    path('markdownx/', include('markdownx.urls')),  # これ
]

# 開発環境でのメディアファイルの配信設定
urlpatterns += static(
    settings.MEDIA_URL,
    document_root=settings.MEDIA_ROOT
)

管理画面でMarkdownエディター

こんな感じで、管理画面でのMarkdown入力をプレビューしてくれます。 1

また、画像のドラッグアンドドロップで自動的に画像がMarkdown形式で挿入されます。 2

これは簡単で、admin.pyを以下のようにするだけです。

from django.contrib import admin
from markdownx.admin import MarkdownxModelAdmin
from .models import Post

admin.site.register(Post, MarkdownxModelAdmin)

自作のページでMarkdownエディター

適当なビューを作ります。

class PostCreate(generic.CreateView):
    model = Post
    fields = '__all__'

そして、次のようにテンプレートに書いておきます。{{ form.media }}を忘れないようにしましょう。

            <form action="" method="POST">
                {{ form.as_p }}
                <button type="submit">送信</button>
            </form>
            {{ form.media }}

ちゃんと動作しますね。 3

{{ form.as_p }}としていますが、{{ form.text }}でもプレビュー欄は表示されます。PostモデルのtextフィールドはMarkdownxFieldとしていましたが、これが面倒な部分を自動で行ってくれます。

通常のフォームで使う

もし通常のフォームで使いたいならば、次のようにします。

from django import forms
from markdownx.fields import MarkdownxFormField


class YourForm(forms.Form):
    text = MarkdownxFormField()

追加のCSSクラス

追加のCSSクラスを設定したい場合は、例えば次のようにします。

from django import forms
from .models import Post
from markdownx.widgets import MarkdownxWidget


class PostCreateForm(forms.ModelForm):

    class Meta:
        model = Post
        fields = '__all__'
        widgets = {
            'text': MarkdownxWidget(attrs={'class': 'textarea'}),
        }

MarkdownをHTMLとして表示

Markdownでかっこよく書けたので、それをテンプレートで表示しましょう。

MarkdownをHTMLに変換するテンプレートタグを作ってもいいのですが、今回のようにモデルを使うならば、モデルのメソッドとして定義しておくのシンプルで良いかもしれません。

from django.db import models
from markdownx.models import MarkdownxField
from markdownx.utils import markdownify


class Post(models.Model):
    text = MarkdownxField('本文', help_text='Markdown形式で書いてください。')

    def text_to_markdown(self):
        return markdownify(self.text)

テンプレートで、次のようにします。これで#h1要素になりますし、##h2...といった具合になっていきます。

{{ post.text_to_markdown | safe }}

Bulmaを使っている場合

通常の表示時

CSSフレームワークのBulmaでは、<h1>ハロー</h1>のようにしても文字は小さいままで、<h1 class="title">ハロー</h1>のように適切なclassを設定する必要があります。これはh1に限った話ではありません。

しかしMarkdownをHTMLに変換した際は、基本的にclassの設定はされていません。

そのような場合に備えて、Bulmaでは次のようにします。

<div class="content">
    {{ post.text_to_markdown | safe }}
</div>

このcontentの中にあるそれぞれの要素は、class属性がなくてもそれっぽく表示されるようになります。つまり、<h1>ハロー</h1>がちゃんと大きく表示されるというわけです。WYSIWYGやMarkdownにもちゃんと対応しているのです。

プレビューでの表示時

プレビューの際も、上のようにそれっぽく表示されてほしいはずです。そのままでは、h1もpも同じ大きさです。

これをするには、MarkdownxFieldが使っているウィジェットクラスのテンプレートを上書きする必要があります。フォームのウィジェットというのは、それがどういうHTMLになるかをテンプレートで指定します。Django標準のウィジェットテンプレートや、今回のようにサードパーティ製アプリのウィジェットテンプレートも自由に上書きすることができます。

そのためには、いくつか追加の設定が必要です。settings.pyに、以下のように設定しておきます。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  # これ
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'blog.context_processors.common',
            ],
        },
    },
]

DIRSに何か指定するようにしましょう。自分のアプリケーション内で他ライブラリのウィジェットテンプレートを上書きするとややこしいので、上書き用のテンプレートディレクトリを作る方がわかりやすいです。今回の例は、プロジェクト直下にtemplatesディレクトリを作り、そこで上書きします。

上書きに限らず、プロジェクトのテンプレートを全てここで管理する人も多くいますね。

一般的なテンプレートの上書きはこれで良いのですが、今回のようにフォームのウィジェットテンプレートを上書きするにはもう少し作業がいります。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.forms',  # 足す
    'app.apps.AppConfig',  # マイアプリケーション
    'markdownx',  # マークダウンライブラリ
]

# ウィジェットテンプレートを上書きするための設定
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'

これで、既存のウィジェットのテンプレートを上書きできるようになります。テンプレートの上書きや探索順序については、Django、テンプレートの探索順序も御覧ください。

では、プロジェクト直下のtemplates内にmarkdownx/widget2.htmlを作成しましょう。中身は次のようにします。

<div class="markdownx">
    {% include 'django/forms/widgets/textarea.html' %}
    <div class="markdownx-preview content"></div>
</div>

これはもともとの内容に、contentというclassを足しているだけです。

目次を表示する

私のブログでは、記事に目次をつけています。 目次

この目次によりどういったコンテンツがあるか一目でわかりますし、クリックでその見出し部分に飛ぶことができます。

更に、こういった目次をつけておくとGoogle検索結果にもカッコよく表示されます。 セクションリンク

早速導入してみましょう。django-markdownx(正確には依存しているMarkdownというライブラリ)はいろいろな機能を拡張することができ、この目次もその一つです。

settings.pyを編集しましょう。

# 見出しを使う場合は、tocを入れましょう。
MARKDOWNX_MARKDOWN_EXTENSIONS = [
    'markdown.extensions.toc',
]

後は、本文に[TOC]と入れるだけです。これで、h1やh2、h3といった要素...Markdownで言う#, ##, ###等から自動的に見出しを作ってくれます。

[TOC]

## 概要
...
...

ハイライトさせる

プログラムを書くならば、コードをハイライトさせたいと思うでしょう。

まずsettings.pyにて、Fenced Code Blocks導入します。

MARKDOWNX_MARKDOWN_EXTENSIONS = [
    'markdown.extensions.extra',  # Fenced Code Blocksは、これに含まれている
    'markdown.extensions.toc',
]

プログラムを次のように書けるようになります。

```python
import a
import b
```

これは<pre><code class="Python">...</code></pre>のように出力され、class="Python"のように言語の指定ができるので、プログラムのハイライトがしやすくなります。

highlight.jsを使うでも紹介していますが、ページ中のコードをハイライトさせるには次のようにします。

<div class="content">
    {{ post.text_to_markdown | safe }}
</div>
...
...
<!-- highlight.js関連の読み込み -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/dracula.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>

新規作成や更新時の、プレビュー欄内のコードをハイライトさせるには次のようにします。

<form action="" method="POST">
    {{ form.as_p }}
    <button type="submit">送信</button>
</form>


{{ form.media }}
<!-- highlight.js関連の読み込み -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/dracula.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script>

const elements = document.getElementsByClassName('markdownx');
for (element of elements) {
    element.addEventListener('markdownx.update', event => {
        for (const block of document.querySelectorAll('pre code')) {
            hljs.highlightBlock(block);
        }
    });
}
</script>

django-markdownxはマークダウンテキストが変更されると、

  1. マークダウンテキストをDjango側のビューに送信

  2. マークダウンをHTMLに変換し、返す

  3. そのHTMLをプレビュー欄に反映

という処理をしています。この際のイベントはキャッチすることができ、それがmarkdownx.updateです。結果として、プレビュー内のHTMLが変更されるたびにハイライトをしなおすという処理になります。

リサイズ処理の設定

デフォルトでは、アップロードした画像の最大サイズは500*500です。もう少し大きい画像もアップしたいということであれば、settings.pyに以下のように追記しましょう。

# 2000, 2000 ぐらいの画像まではリサイズさせない。
MARKDOWNX_IMAGE_MAX_SIZE = {'size': (2000, 2000), 'quality': 100}