django-markdownxの紹介

2018-12-19 / 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に変換するには、テンプレートフィルタを作るのがシンプルです。アプリケーションディレクトリ内にtemplatetagsディレクリを作り、適当なpythonファイル...ここではapp.pyとしますが、それを作ります。

中身は次のようにしておきます。

from django import template
from django.utils.safestring import mark_safe
import markdown
from markdownx.utils import markdownify
from markdownx.settings import (
    MARKDOWNX_MARKDOWN_EXTENSIONS,
    MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS
)
from markdown.extensions import Extension

register = template.Library()


@register.filter
def markdown_to_html(text):
    """マークダウンをhtmlに変換する。"""
    return mark_safe(markdownify(text))


class EscapeHtml(Extension):
    def extendMarkdown(self, md):
        md.preprocessors.deregister('html_block')
        md.inlinePatterns.deregister('html')


@register.filter
def markdown_to_html_with_escape(text):
    """マークダウンをhtmlに変換する。

    生のHTMLやCSS、JavaScript等のコードをエスケープした上で、マークダウンをHTMLに変換します。
    公開しているコメント欄等には、こちらを使ってください。

    """
    extensions = MARKDOWNX_MARKDOWN_EXTENSIONS + [EscapeHtml()]
    html = markdown.markdown(text, extensions=extensions, extension_configs=MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS)
    return mark_safe(html)


markdown_to_html関数は、マークダウンをHTMLに変換するテンプレートフィルタです。#h1要素になりますし、##h2...といった具合になっていきます。mark_safe関数は何かと言うと、テンプレートにHTMLが入った変数をそのまま埋め込むことはできず、埋め込みたい場合はmark_safe関数を呼び出しておく必要がある為です。mark_safe関数の代わりに、テンプレート側でsafeテンプレートフィルタ、autoescapeテンプレートタグを使っても大丈夫です。markdownify関数は、マークダウンを実際にHTMLに変換する処理をしてくれます。

markdown_to_html_with_escape関数は、公開しているコメント欄等で使うフィルタです。マークダウンを評価するライブラリの多くでは、生のHTMLやCSS、JavaScriptコードが書けます。つまり、公開しているコメント欄等で悪質なJavaScriptコード等を埋め込まれる可能性があるということです。その対策として、少し回りくどい処理をしています。

自分で書く本文には、場合によってHTMLやJavaScriptを直接書きたい場合もあるかもしれませんので、markdown_to_htmlを使うとよいでしょう。コメント欄などにはmarkdown_to_html_with_escapeを使いましょう

テンプレートで、次のようにしてマークダウンテキストをHTMLに変換できます。{% load app %}のようにして、自分で作ったテンプレートフィルタのあるapp.pyを読み込ませる必要があります。

{% extends 'blog/base.html' %}
{% load app %}
...
...
<!-- 記事の本文 -->
{{ post.text | markdown_to_html }}

<!-- コメントの表示 -->
{{ comment.text | markdown_to_html_with_escape %}

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標準のウィジェットテンプレートや、今回のようにサードパーティ製アプリのウィジェットテンプレートも自由に上書きすることができます。

上書きをするには、templates内にmarkdownx/widget2.htmlを作成するだけです。中身は次のようにします。

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

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

プロジェクト直下にtemplatesディレクトリを置いて管理している方は、次のような設定が必要です。

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

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

詳しくはDjango、テンプレートの探索順序をご覧ください。

目次を表示する

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

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

更に、こういった目次をつけておくと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}

この記事の関連記事

highlight.jsを使う

2018-11-06 / JavaScriptライブラリ

- コードのハイライトを行う「highlight.js」を紹介します。マークダウンとの相性がよく、導入もしやすく、使い勝手がいいプラグインです。

Django、テンプレートファイルの探索順序について

2019-02-26 / PythonDjango

- Djangoのテンプレートが探される順序を説明していきます。通常のテンプレートのほか、フォームウィジェットのテンプレートにも触れていきます。

Djangoで、ブログアプリの本文に画像を挿入する方法まとめ

2019-05-14 / PythonDjango

- ブログアプリケーションを作成した際の、記事本文へ画像を差し込む方法についてよく質問が来ますので、それについてを説明していきます。これは記事をどうやって書いているかによって、方法が変わります。

コメント欄

記事にコメントする

名無し

django-markdownxを導入しようとしたとこ躓いてしまったので、ご質問です。

私はanacondaでpythonを導入したので、極力conda install のコマンドでパッケージを導入しています。 anacondaではdjango-markdownxをサポートしていないようでしたので、pip installでやってみたのですがエラーが出て導入できませんでした。エラーメッセージは下記の通りです。

「Could not install packages due to an EnvironmentError: [Errno 2] No such file or directory: 'c:\users\luckm\anaconda3\lib\site-packages\pytz-2018.5.dist-info\METADATA'」

上記のようなエラーが出るのはanacondaを使っているのが原因かなと考えています。 narito様はanacondaは使わず、pythonを直接インストールされましたでしょうか?

コメントに返信する

名無し

先程質問した者です。 仮想環境を作ってやり直したところ問題なくできました。

しかし、修正したdjango-projectをサーバーへデプロイしようとしたところ躓いてしまいました。 サーバー上でsettings.pyの末尾にSTATIC_ROOT = '/var/www/static'と記入し、sudo python3.6 manage.py collectstaticでとしたところ、ModuleNotFoundError: No module named 'markdownx'と表示されました。

pip3.6 freezeで確認するとdjango-markdownxが入っているのを確認しているのですが、何故No moduleとなってしまうのでしょうか?

なりと

恐らくsudo python3.6 としたときに読み込むライラブリ置き場と、pip3.6としたときに読み込むライブラリ置き場が違っている状態です。

pythonpipが指しているPythonの実行環境がそれぞれ違う、ということが稀にあります。Anacondaやpyenvを使ったり、知らないうちにpython3.6環境を複数作ってしまった、等のケースです。また、sudoでのコマンド検索パスで更にややこしくなっていることもあります。

解決方法はいくつかありますが、sudo python3.6 -m pip install django-markdownxとしてインストールすると殆どの場合解決できます。sudo python3.6 -m pipは、sudo python3.6の環境にあるpipを使ってくれますので、pythonとpipの環境がズレることは起きません。

名無し

ありがとうございます。 ご指摘いただいた内容を参考に修正したら問題なく起動しました。

名無し

大変参考になりました。有益な記事をありがとうございます。

一つ質問なのですが、記事の目次を[TOC]にて管理画面のMarkdown入力箇所に表示させることは出来ましたが、これだと通常ページでは記事の上部もしくは下部にしか目次を表示させることが出来ません。Qiitaのように、画面の右側もしくは左側に目次を表示させるにはどうしたらよろしいでしょうか。TOCを別個に抽出する必要があるように思い、調べてみましたが、唯一中国語のページでそれっぽい記事がありましたが、よく分かりませんでした。

ご教示いただければ幸いです。

コメントに返信する

なりと

私のブログでは、次のような感じで実装しています。記事内の目次をコピーし、別の場所に張り付けて、記事内の目次を削除しています。

// 記事内の目次要素
const toc = document.querySelector('div.markdown-body > div.toc > ul');

// 目次を複製する
const copyToc = toc.cloneNode(true);

// 複製した目次を、別の場所に張り付ける。サイドバーに<nav id="toc">がある例
document.querySelector('nav#toc').appendChild(copyToc);

// 記事内の目次を削除する
document.querySelector('div.markdown-body > div.toc').removeChild(toc);
名無し

お忙しい中、迅速に返信いただき、どうもありがとうございました。

なるほど、そのようにされているんですね。 早速、いただいたヒントをもとに、自分の環境での実装にとりくんでまいります。

いつも有益な記事を提供して下さる、本ブログの発展を陰ながら応援いたしております。

どうもありがとうございました。

名無し

Markdownxに関する詳細な記事をどうもありがとうございます。大変参考になりました。一つ質問なのですが、Markdownをテンプレートに表示させた際の、見出し等の見た目のスタイルのカスタマイズはどのようにしたらよいのでしょうか。Markdownxをインストールして静的ファイルを集めた際にできるcssファイル(markdownx.css)の中身をいじっても何も起こりません。本ページのように見出し文の下に下線を引くなどかっこいい表示ができたらと考えております。お手すきの時で結構ですので、よろしくお願いします。

コメントに返信する

なりと

テンプレートファイルにて、次のように自作のCSSファイルを読み込ませるか

{% load static %}
...
...

<link rel="stylesheet" href="{% static  'app/style.css' %}">

又はstyle要素を作り、その中に直接CSSを記述してください。

<style>
    h2 {
        font-size: 30px;
        font-weight: bold;
        border-bottom: solid 5px #000;
        padding-bottom: 5px;
    }
</style>

上のようにスタイルを記述するとすべてのh2要素に対して適用されてしまいます。マークダウンで書いていない部分のh2にも適用されるということです。

なので、マークダウンテキスト部分を次のようにdiv要素等で囲んでおくことをオススメします。

<div class="markdown">
    {{ post.text | markdown_to_html }}
</div>

そうすると、CSSは次のように書けます。これならば、マークダウンテキストにだけ適用されます。

<style>
    div.markdown h2 {
        font-size: 30px;
        font-weight: bold;
        border-bottom: solid 5px #000;
        padding-bottom: 5px;
    }
</style>
名無し

返信ありがとうございます。教えていただいたとおりに行ったところ、出来ました! お忙しいところどうもありがとうございました!