【Python】Python-Markdownで拡張機能を追加する(その2)


投稿日 2021年2月21日 >> 更新日 2023年3月1日

概要

今回はPython外部ライブラリのPython-Markdownで拡張機能を追加する(その2)です。

(その1)では拡張機能の「extra」について主に実装してきましたが、この記事ではextraで使える7つのマークダウン以外の拡張機能を使用して1つひとつ実装していこうと思います。

デフォルトで使用できるマークダウンや拡張機能の「extra」を設定したマークダウンについては別記事にて実装しているので宜しければ以下をご参照ください。

実行環境&使用ライブラリ

実行環境
Windows Subsystem for Linux
Python 3.6.9
pip 9.0.1
使用ライブラリ ライセンス
Markdown==3.3.3 BSD

拡張機能を設定する方法

拡張機能を設定する場合は、markdown.markdown()関数もしくわmarkdown.Markdown()クラスのパラメータ引数extensionsextension_configsにそれぞれリストや辞書の要素を渡します。

例えば私の記事の冒頭に「目次」がありますが、拡張機能のtocを使って自動で内部リンクが施されたリストとして表示されています。


import markdown

# 冒頭に[TOC]を追記
text = """
[TOC]

# Hello World 1
## Hello World 2
### Hello World 3
#### Hello World 4
"""

html = markdown.markdown(text, extensions=['toc']) # 'toc'の設定

print(html)
"""
<div class="toc">
<ul>
<li><a href="#hello-world-1">Hello World 1</a><ul>
<li><a href="#hello-world-2">Hello World 2</a><ul>
<li><a href="#hello-world-3">Hello World 3</a><ul>
<li><a href="#hello-world-4">Hello World 4</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<h1 id="hello-world-1">Hello World 1</h1>
<h2 id="hello-world-2">Hello World 2</h2>
<h3 id="hello-world-3">Hello World 3</h3>
<h4 id="hello-world-4">Hello World 4</h4>
"""

パラメータ引数のextensionstocを設定し、テキストの冒頭に「[TOC]」とするだけでリンク付きの目次を生成することができました。

ブラウザでは以下のような表示です。

そしてパラメータ引数のextension_configsでは、extensionsで設定した機能を細部にわたり設定することができます。

拡張機能をさらに拡張するといったイメージでしょうか。

先ほどの設定では目次である<ul>タグや<li>タグが「[TOC]」によって自動生成されましたが、タイトルも自動生成されると助かります。

そこでextension_configsに以下のような辞書を渡すことで拡張することができます。


import markdown


text = """
[TOC]

# Hello World 1
## Hello World 2
### Hello World 3
#### Hello World 4
"""

html = markdown.markdown(
    text,
    extensions=['toc'],
    extension_configs={
        'toc':{
            'title': '目次' # 目次にタイトルを設ける
        }
    }
)

print(html)
"""
<div class="toc"><span class="toctitle">目次</span><ul>
<li><a href="#hello-world-1">Hello World 1</a><ul>
<li><a href="#hello-world-2">Hello World 2</a><ul>
<li><a href="#hello-world-3">Hello World 3</a><ul>
<li><a href="#hello-world-4">Hello World 4</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<h1 id="hello-world-1">Hello World 1</h1>
<h2 id="hello-world-2">Hello World 2</h2>
<h3 id="hello-world-3">Hello World 3</h3>
<h4 id="hello-world-4">Hello World 4</h4>
"""

目次のタイトルとして「目次」が表示されているのが分かります。

ブラウザでの結果。

このようにPython-Markdownでは拡張機能が幾つか備わっているので、幾つでもリストや辞書内に追加をしてマークダウン記法を増やすことができます。

モジュールに備わっている拡張機能一覧はこちらです。

拡張機能によるマークダウン一覧(その2)

Python-Markdownの拡張機能は、extensions引数に追加したい機能を設定する事でデフォルトでは使えなかったマークダウン記法を認識します。

デフォルトで使用できるマークダウン記法一覧と「extra」拡張機能の一覧はこちらです。

内容 マークダウン(半角記号) HTML(頭タグ)
admonition
(訓戒・忠告)
!!! ..."..." <div class="admonition ...">
<p class="admonition-title">...
codehilite
(コードハイライト)
```
code
```

~~~
code
~~~
<div class="codehilite">
<pre><span></span>
<span class="n">code...
legacy_attrs
(レガシー属性)
[リンク](/path/){@target=_blank} <p>
<a href="/path/" target="_blank">リンク..
legacy_em
(レガシー強調)
_..._..._ <p><em>...</em>..._
nl2br
(ブレークする改行)
...
...
<p>...
...
sane_lists
(正常なリスト)
3. ...
4. ...
<ol start="3">
<li>...
toc
(目次)
[TOC] <div class="toc">
<ul>
<li>
<a href="#...">...
wikilinks
(ウィキリークス)
[[...]] <p><a class="wikilink" href="/.../">...
meta
(メタデータ)
...: ... HTMLでは無視され、メタデータとして取得できる
smarty 置き換え記号 ASCII記号をHTMLエンティティに変換

各種の拡張機能

Markdown==3.3.3ではおよそ17種類の拡張機能が備わっています。

今回実装する10種類はリストオブジェクトに1つひとつ追加設定していく必要があります。


import markdown

extensions = [
    'admonition',
    'codehilite',
    'legacy_attrs',
    'legacy_em',
    'nl2br',
    'sane_lists',
    'toc',
    'wikilinks',
    'meta',
    'smarty',        
]

html = markdown.markdown(text, extensions=extensions)

特に必要のない機能は省いたりすることができるので、省かれた機能はマークダウンとしても無効になります。

Python-Markdownで備わっている拡張機能の一覧は以下の公式ドキュメントでも確認することができます。

admonitionによる訓戒・忠告

訓戒もしくは忠告は、その名の通り読者側に対する注意書きです。

このマークダウンを記述することによりCSSのクラス属性を付与することができるので、他の文章とは見た目の異なるブロックで表現することができます。

記述方法は、段落の1列目からビックリマーク3つ(!!!)記載したあとに、CSSのクラス属性の名前を記入し半角スペースを空けてからそのブロックのタイトル名をダブルクォート(")により囲みます。

ブロック内で囲まれた文章の内容は、2段落目の半角スペース4つを空けてから記述していきます。


import markdown

text = """
!!! add_class_name "タイトル"
    忠告や警告したい内容

    2段落目

"""

html = markdown.markdown(text, extensions=['admonition'])
print(html)
"""
<div class="admonition add_class_name">
<p class="admonition-title">タイトル</p>
<p>忠告や警告したい内容</p>
<p>2段落目</p>
</div>
"""

CSSでクラス属性の名前を指定して色を変更してみると以下のように読者の注意を引き付けることができます。

クラス属性の名前は幾つでも付与することができ、タイトルが不要な場合は記述しなくても機能します。


text = """
!!!class_name_1 class_name_2 ""
    忠告や警告したい内容

    2段落目

"""

html = markdown.markdown(text, extensions=['admonition'])
print(html)
"""
<div class="admonition class_name_1 class_name_2">
<p>忠告や警告したい内容</p>
<p>2段落目</p>
</div>
"""

クラス属性の名前に応じて注意書き文のバックカラーなどを変えると読者も分かりやすそうです。

codehiliteによるコードハイライト

コードハイライトはコードブロックに特化した拡張機能で、Python外部ライブラリの「Pygmets」というライブラリと併用して使うことにより実装することができます。

簡単に説明すると、コードブロック内における段落の設定や特定の行の強調表示などのマークダウン、様々なテーマが用意されたシンタックスハイライトのスタイルシートなど、バックエンド側で解決してしまいます。

詳しい内容はPygments公式ドキュメントをご参照ください。

Pygmentsはpipでインストールします。

$ pip install pygments

コードブロックをマークダウンで使用するためには、拡張機能fenced_codeが必要なので、{extensions{.c-h}引数にはfenced_code{.c-h}とcodehilite`{.c-h}を設定します。


text = """
```
import markdonw

md = markdown.Markdown(extentions=['fenced_code', 'codehilite'])
```
"""

html = markdown.markdown(text, extensions=['fenced_code', 'codehilite'])
print(html)
"""
<div class="codehilite"><pre><span></span><span class="kn">import</span> <span class="nn">markdonw</span>

<span class="n">md</span> <span class="o">=</span> <span class="n">markdown</span><span class="o">.</span><span class="n">Markdown</span><span class="p">(</span><span class="n">extentions</span><span class="o">=</span><span class="p">[</span><span class="s1">&#39;fenced_code&#39;</span><span class="p">,</span> <span class="s1">&#39;codehilite&#39;</span><span class="p">])</span>
</pre></div>
"""

クラス属性codehilitedivタグに囲まれたコードブロックが出力されました。

これはPython-MarkdownのバックエンドでPygmentsエンジンが呼び出されており自動言語検出によってシンタックスハイライトの為のHTMLタグやその属性が自動で付与されています。

pygmentsライブラリでは様々なシンタックスハイライトのテーマが用意されており、2つの方法でそのテーマを適用させることができます。

  1. pygmentizeコマンドでCSSファイルをダウンロードします。

  2. バックエンドの設定でテーマを適用する。

の2つです。

まずは1つ目の流れを簡単に説明します。

Pygmentsで用意されているファイル一覧は以下のコマンドで取得できます。

$ pygmentize -L style

Styles:
~~~~~~~
* default:
    The default style (inspired by Emacs 22).
* emacs:
    The default style (inspired by Emacs 22).
...

defaultというテーマのシンタックスハイライトをダウンロードするとしたら、以下のようにコマンドを打ちます。

$ pygmentize -S default -f html -a .codehilite > default.css
  • 「-S」はテーマの指定
  • 「-f」はフォーマットの指定
  • 「-a」はクラス属性の指定
  • 「> 名前.css」(指定したディレクトリにCSSファイルが配置される)

ダウンロードしたCSSファイルを「<link>」タグで読み込ませるだけでシンタックスハイライトできます。

2つ目の方法がバックエンドでの設定です。

Python-Markdownでは拡張機能を設定する際にextensions引数に渡して機能を使用できるようになりましたが、拡張機能自体の設定はextension_configsという引数に辞書型の要素を渡すことによりデフォルトの振る舞いを変更できます。

codehilite拡張機能のデフォルトでは、noclassesキーの値がFalseとなっているので、Trueに変更してシンタックスハイライトのスタイルを有効にします。


...

configs = {
    'codehilite':{
        'noclasses': True
    }
}

html = markdown.markdown(
    text,
    extensions=['fenced_code', 'codehilite'],
    extension_configs=configs
)
print(html)
"""
<div class="codehilite" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">import</span> <span style="color: #0000FF; font-weight: bold">markdonw</span>

md <span style="color: #666666">=</span> markdown<span style="color: #666666">.</span>Markdown(extentions<span style="color: #666666">=</span>[<span style="color: #BA2121">&#39;fenced_code&#39;</span>, <span style="color: #BA2121">&#39;codehilite&#39;</span>])
html <span style="color: #666666">=</span> md<span style="color: #666666">.</span>convert(text)
</pre></div>
"""

シンタックスハイライトのテーマを変更したい場合はpygments_styleキーにテーマを指定します。

※テーマは先ほど取り上げたpygmentizeコマンドで一覧を取得できます。


configs = {
    'codehilite':{
        'pygments_style': 'solarized-dark', # デフォルトはdefault
        'noclasses': True
    }
}

下図は上記の設定の例です。

その他の詳細設定は公式ドキュメントをご参照ください。

本題に戻りまして、マークダウンで段落の番号を表示させたい場合は、コードブロック内の最初の段落1列目に「#!」を記述します。

text = """
```
#!
import markdonw

md = markdown.Markdown(extentions=['fenced_code', 'codehilite'])
```
"""

configs = {
    'codehilite':{
        'pygments_style': 'default',
        'noclasses': True
    }
}

html = markdown.markdown(
    text,
    extensions=['fenced_code', 'codehilite'],
    extension_configs=configs
)
print(html)
"""
<table class="codehilitetable"><tr><td><div class="linenodiv" style="background-color: #f0f0f0; padding-right: 10px"><pre style="line-height: 125%">1
2
3</pre></div></td><td class="code"><div class="codehilite" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">import</span> <span style="color: #0000FF; font-weight: bold">markdonw</span>

md <span style="color: #666666">=</span> markdown<span style="color: #666666">.</span>Markdown(extentions<span style="color: #666666">=</span>[<span style="color: #BA2121">&#39;fenced_code&#39;</span>, <span style="color: #BA2121">&#39;codehilite&#39;</span>])
</pre></div>
</td></tr></table>
"""

次はコードブロック内の特定の行を指定し、背景を目立たせて読者の注意を引き付けます。

最初の段落の1列目にコロン3つとpython(:::python)を記述し、半角スペースのあとに「hl_lines=""」で強調したい段落もしくは行番を指定します。


text = """
```
:::python hl_lines="3"
import markdonw

md = markdown.Markdown(extentions=['fenced_code', 'codehilite'])
```
"""

...

html = markdown.markdown(
    text,
    extensions=['fenced_code', 'codehilite'],
    extension_configs=configs
)
print(html)
"""
<div class="codehilite" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">import</span> <span style="color: #0000FF; font-weight: bold">markdonw</span>

<span style="background-color: #ffffcc">md <span style="color: #666666">=</span> markdown<span style="color: #666666">.</span>Markdown(extentions<span style="color: #666666">=</span>[<span style="color: #BA2121">&#39;fenced_code&#39;</span>, <span style="color: #BA2121">&#39;codehilite&#39;</span>])
</span></pre></div>
"""

複数の段落を指定する場合は、「hl_lines="3 6"」のように数字と数字の間は半角スペースを置きます。


text = """
```
:::python hl_lines="3 6"
import markdonw

text = '#Hello'

md = markdown.Markdown(extentions=['fenced_code', 'codehilite'])
html = md.convert(text)
```
"""

...

html = markdown.markdown(
    text,
    extensions=['fenced_code', 'codehilite'],
    extension_configs=configs
)
print(html)
"""
<div class="codehilite" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">import</span> <span style="color: #0000FF; font-weight: bold">markdonw</span>

<span style="background-color: #ffffcc">text <span style="color: #666666">=</span> <span style="color: #BA2121">&#39;#Hello&#39;</span>
</span>
md <span style="color: #666666">=</span> markdown<span style="color: #666666">.</span>Markdown(extentions<span style="color: #666666">=</span>[<span style="color: #BA2121">&#39;fenced_code&#39;</span>, <span style="color: #BA2121">&#39;codehilite&#39;</span>])
<span style="background-color: #ffffcc">html <span style="color: #666666">=</span> md<span style="color: #666666">.</span>convert(text)
</span></pre></div>
"""

legacy_attrsによるレガシー属性

レガシー属性は、別の拡張機能の「attr_list(属性リスト)」以前に使用されていたマークダウンを復元するということで、現在は「attr_list」が推奨されているみたいです。

「legacy_attrs」のマークダウンは、中括弧内にアットマークと属性({@ 属性=要素})といった形で文章中に記述することができます。


text = """
![画像{@class=img}](path/to/image.png)

[リンク](/path/){@target=_blank}

Some *emphasized*{@id=bar} text.

"""

html = markdown.markdown(text, extensions=['legacy_attrs'])
print(html)
"""
<p><img alt="画像" class="img" src="path/to/image.png" /></p>
<p><a href="/path/" target="_blank">リンク</a></p>
<p>Some <em id="bar">emphasized</em> text.</p>
"""

ただし複数の属性を指定すると、最後に指定した属性以外のダブルクォートが生成されないといった問題があります。


text = """
![画像](path/to/image.png){@id=img class=img title=img}
"""

html = markdown.markdown(text, extensions=['legacy_attrs'])
print(html)
"""
<p><img alt="画像" id=img class=img title="img" src="path/to/image.png" /></p>
"""

なので、属性の設定では拡張機能のattr_listを使用するのが良いかと思います。


# {: ...}に変更
text = """
![画像](path/to/image.png){:id=img class=img title=img}
"""

html = markdown.markdown(text, extensions=['attr_list']) # 変更
print(html)
"""
<p><img alt="画像" class="img" id="img" src="path/to/image.png" title="img" /></p>
"""

lagecy_emによるレガシー強調

レガシー強調では、アンダーラインで囲まれた文章は必ず斜体化するといった設定です。

デフォルトでは、「_str_str_」のような文章でも賢く「<em>str_str</em>」と変換を行いますが、拡張機能の「lagech_em」を設定することにより「<em>str</em>str_」のように末端は無視して変換します。


text = """
_str_and_
"""

html = markdown.markdown(text, extensions=['legacy_em'])
print(html)
"""
<p><em>str</em>and_</p>
"""

nl2brによる改行

改行では、<p>タグ内での改行が有効となります。


text = """
Line
Line
Line

Line
"""

html = markdown.markdown(text, extensions=['nl2br'])
print(html)
"""
<p>Line<br />
Line<br />
Line</p>
<p>Line</p>
"""

sane_listsによる正常なリスト

正常なリストでは、番号付きリストや通常のリストを混在させないように動作します。

デフォルトでは異なるリストを段落区切りでマークダウンすると、最初に認識されたリストと混同してしまいます。

# デフォルトの動作

text = """
1. list1
2. list2

* list
* list
"""

html = markdown.markdown(text)
print(html)
"""
<ol>
<li>list1</li>
<li>
<p>list2</p>
</li>
<li>
<p>list</p>
</li>
<li>list</li>
</ol>
"""

拡張機能sane_listsを設定すると、ブロックごとに認識して変換されます。


text = """
1. list1
2. list2

* list
* list
"""

html = markdown.markdown(text, extensions=['sane_lists'])
print(html)
"""
<ol>
<li>list1</li>
<li>list2</li>
</ol>
<ul>
<li>list</li>
<li>list</li>
</ul>
"""

番号付きリストの開始番号も記述通りに認識され変換されるようになります。


text = """
1. list1
2. list2

next

3. list3
4. list4
"""

html = markdown.markdown(text, extensions=['sane_lists'])
print(html)
"""
<ol>
<li>list1</li>
<li>list2</li>
</ol>
<p>next</p>
<ol start="3">
<li>list3</li>
<li>list4</li>
</ol>
"""

tocによる目次

目次は、エディター内に「[TOC]」と記述することで、そのマークダウン以降の<h1>や<h2>などのheaderタグをリストとして生成します。

拡張機能tocを設定することにより有効となります。


text = """
[TOC]

# header1
## header2
"""

html = markdown.markdown(text, extensions=['toc'])
print(html)
"""
<div class="toc">
<ul>
<li><a href="#header1">header1</a><ul>
<li><a href="#header2">header2</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="header1">header1</h1>
<h2 id="header2">header2</h2>
"""

上図では「[TOC]」により生成されたリストにアンカーリンクが付与されています。

各リンクはheaderに生成されたid属性に移動することができますが、移動先のheaderにはリンクが生成されていません。

headerにパーマリンクを設定してあげると、読者の方もブックマークをしやすくなると思うので拡張機能「toc」の設定を一部変更します。

extension_configs引数に以下のような辞書を渡します。


text = """
[TOC]

# header1
## header2
"""

# headerにパーマリンクを生成
configs = {
    'toc': {
        'permalink': True
    }
}

html = markdown.markdown(
    text,
    extensions=['toc'],
    extension_configs=configs, # 追記
)
print(html)
"""
<div class="toc">
<ul>
<li><a href="#header1">header1</a><ul>
<li><a href="#header2">header2</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="header1">header1<a class="headerlink" href="#header1" title="Permanent link">&para;</a></h1>
<h2 id="header2">header2<a class="headerlink" href="#header2" title="Permanent link">&para;</a></h2>
"""

他にも目次の要素だけ取得したい場合やコアな構成設定を変更したい場合は以下をご参照ください。

ウィキリークは、Wikipedia専用リンクと言っていいでしょうか。詳細は分かりませんが、二重角括弧内の単語([[単語]])をリンク名とリンク先(<a href=/単語/>単語</a>)に変換されます。

その他にもデフォルト値では「class="wikilink"」という属性が設定され、CSSにて他のリンクとは異なったスタイルに変更できます。

拡張機能wikilinksを設定してマークダウンを記述してみると以下のようになります。


text = """
[[ウィキ]]
"""

html = markdown.markdown(text, extensions=['wikilinks'])
print(html)
"""
<p><a class="wikilink" href="/ウィキ/">ウィキ</a></p>
"""

二重角括弧内の単語「ウィキ」が<a>タグのhref属性に設定されているのが分かります。

このマークダウンをWikipedia専用のリンクとして使用するのであれば、href属性には予めWikipediaのURLを設定しておく必要があります。

拡張機能wikilinksの設定を変更するには幾つか方法がありますが、この記事ではextension_config引数に設定内容を渡していきます。

設定するオプションは、ベースとなるURLの設定と末尾のURLの設定です。

デフォルトではベースと末尾のURLは共にスラッシュ(/)となっています。

ついでに拡張機能attr_listを追加してリンク先のページが別タブになるようtarget属性をマークダウンで付与します。


text = """
[[ウィキ]]{: target=_blank}
"""

configs= {
    'wikilinks': {
        'base_url': 'https://ja.wikipedia.org/wiki/',
        'end_url': '',
    }
}

html = markdown.markdown(
    text,
    extensions=['attr_list', 'wikilinks'],
    extension_configs=configs,
)
print(html)
"""
<p><a class="wikilink" href="https://ja.wikipedia.org/wiki/ウィキ" target="_blank">ウィキ</a></p>
"""

他にもクラス属性の値なども変更することができるので宜しければ公式ドキュメントをご参照ください。

metaによるメタデータ

Python-Markdownのメタデータとは、エディタ内で特定のマークダウンが記述された情報をメタデータとして扱います。

メタデータとして扱われる文章はHTMLに変換される前に削除されメタデータ以外の文章だけが出力されます。

記述方法は、段落1列目にメタデータとして記載する単語の末尾にコロン(:)、半角スペースをあけてから単語の内容文を記載します(単語: 単語の内容文)。

"""
タイトル: 内容
日付: 〇月〇日

本文
"""

拡張機能metaを使用するには、Markdownクラスで初期化して、convert()メソッドによりテキストを解析してから出力します。

※以下は公式通りの例となります


import markdown

text = """
Title:   My Document
Summary: A brief description of my document.
Authors: Waylan Limberg
         John Doe
Date:    October 2, 2007
blank-value:
base_url: http://example.com

This is the first paragraph of the document.
"""

md = markdown.Markdown(extensions=['meta'])
html = md.convert(text)
print(md.Meta)
# {}

上記のように処理を行うと、md.Meta内には辞書型によるメタデータが格納されているはずですが、私のシステム下ではインタープリターやJupyter notebook共に取得できませんでした。

代わりにWebフレームワーク上ではメタデータを取得することができたので掲載しておきます。

smartyによるSmartyPants

SmartyPantsは、SmartyPantsライブラリをインストールすることで特定のASCII記号をHTMLエンティティへ変換をすることができます。

$ pip install smartypants

例えば、「«」の記号をHTMLエンティティに表すと「&laquo;」のように記述することができますが、拡張機能「smarty」を設定することにより「<<」記号は「«」と認識して変換されます。

ここでは簡単な実装に留めますが、気になる方は公式ドキュメントをご参照ください。

ほんの1例ですが拡張機能smartyの実装です。


text = """
<<Hello>>
"""

# 角度付き引用符を有効にする
configs = {
    'smarty': {
        'smart_angled_quotes': True,
    }
}

html = markdown.markdown(
    text,
    extensions=['smarty'],
    extension_configs=configs, # 設定
)
print(html)
"""
<p>&laquo;Hello&raquo;</p>
"""

サードパーティーの拡張機能について

Python-Markdownにデフォルトで備わっている拡張機能は以上となりますが、他にもサードパーティー製で色々な種類の拡張機能があります。

どんどんマークダウンエディターをカスタイマイズして執筆効率を上げていきましょう。

それでは以上となります。

最後までご覧いただきありがとうございました。