チュートリアル2 マークダウンレンダリングの変更

《 初回公開:2022/03/26 , 最終更新:未 》

原文は

【 目次 】

イントロダクション

Python-Markdownの多くの拡張機能は新しい構文を追加しますが、場合によっては、Markdownが既存の構文をレンダリングする方法を単純に変更したいことがあります。
または、一部の画像をインラインで表示したいが、外部でホストされている画像を単に画像を指すリンクにする必要がある場合があります。

次のMarkdownが提供されたとします。

![a local image](/path/to/image.jpg)

![a remote image](http://example.com/image.jpg)

Python-Markdownが次のHTMLを返すようにします。

<p><img alt="a local image" src="/path/to/image.jpg" /></p>
<p><a href="http://example.com/image.jpg">a remote image</a></p>

注:このチュートリアルは非常に一般的であり、基本的なPython3開発環境を想定しています。
Python開発の基本的な理解が必要です。

分析

私たちが利用できるオプションを考えてみましょう:

  1. 画像関連のインラインパターンを上書きします。
    これは機能しますが、既存のパターンを変更する必要はありません。
    パーサーは構文を正しく認識しています。
    HTML出力を変更するだけです。

    また、インライン画像リンクと参照スタイルの画像リンクの両方をサポートする必要があります。これには、両方のインラインパターンを再定義して、作業を2倍にする必要があります。

  2. 既存のパターンをそのままにして、Treeprocessorを使用してHTMLを変更します。
    これは、Markdown構文のトークン化を変更するものではありません。
    他のサードパーティの拡張機能によって追加された新しい画像構文であっても、画像を表すものはすべて含まれると確信できます。

上記を前提として、オプション2を使用しましょう。

ソリューション

まず、新しいツリープロセッサを作成しましょう。

from markdown.treeprocessors import Treeprocessor

class InlineImageProcessor(Treeprocessor):
    def run(self, root):
        # Modify the HTML here

Treeprocessorrunメソッドは、ElementTreeオブジェクトを含むroot引数を受け取ります。
そのオブジェクト内のすべてのimg要素を反復処理し、外部URLを含む要素を変更する必要があります。
したがって、runメソッドに次のコードを追加します。

# Iterate over img elements only
for element in root.iter('img'):
    # copy the element's attributes for later use
    attrib = element.attrib
    # Check for links to external images
    if attrib['src'].startswith('http'):
        # Save the tail
        tail = element.tail
        # Reset the element
        element.clear()
        # Change the element to a link
        element.tag = 'a'
        # Copy src to href
        element.set('href', attrib.pop('src'))
        # Copy alt to label
        element.text = attrib.pop('alt')
        # Reassign tail
        element.tail = tail
        # Copy all remaining attributes to element
        for k, v in attrib.items():
            element.set(k, v)

上記のコードについて注意すべき点がいくつかあります。

  1. 後でelement.clear()を使用して要素をリセットするときに属性が失われないように、要素の属性のコピーを作成します。
    同じことがtailにも当てはまります。
    img要素にはtextがないため、それについて心配する必要はありません。
  2. href属性とelement.textは、img要素の要素の異なる属性名に割り当てられるため、明示的に設定します。
    その際、attribからsrc属性とalt属性をpopして、最後のステップで残りのすべての属性をコピーしたときにそれらが存在しないようにします。
  3. 内部画像を指すimg要素に変更を加える必要がないため、コードでそれらを参照する必要はありません(単にスキップされます)。
  4. 外部リンク (startswith('http')) のテストは改善することができるように、読者の演習として残されています。

次に、Extensionサブクラスを使用して新しいTreeprocessorMarkdownに通知する必要があります。

from markdown.extensions import Extension

class ImageExtension(Extension):
    def extendMarkdown(self, md):
        # Register the new treeprocessor
        md.treeprocessors.register(InlineImageProcessor(md), 'inlineimageprocessor', 15)

Treeprocessorを優先度15登録します。これにより、すべてのインライン処理が完了した後に確実に実行されます。

Test 1

すべて一緒に見てみましょう:

ImageExtension.py

from markdown.treeprocessors import Treeprocessor
from markdown.extensions import Extension


class InlineImageProcessor(Treeprocessor):
    def run(self, root):
        for element in root.iter('img'):
            attrib = element.attrib
            if attrib['src'].startswith('http'):
                tail = element.tail
                element.clear()
                element.tag = 'a'
                element.set('href', attrib.pop('src'))
                element.text = attrib.pop('alt')
                element.tail = tail
                for k, v in attrib.items():
                    element.set(k, v)


class ImageExtension(Extension):
    def extendMarkdown(self, md):
        md.treeprocessors.register(InlineImageProcessor(md), 'inlineimageprocessor', 15)

次に、拡張機能をMarkdownに渡します。

Test.py

import markdown

input = """
![a local image](/path/to/image.jpg "A title.")

![a remote image](http://example.com/image.jpg  "A title.")
"""

from ImageExtension import ImageExtension
html = markdown.markdown(input, extensions=[ImageExtension()])
print(html)

また、python Test.pyを実行すると、次の出力が正しく返されます。

<p><img alt="a local image" src="/path/to/image.jpg"  title="A title."/></p>
<p><a href="http://example.com/image.jpg" title="A title.">a remote image</a></p>

成功! 各画像にタイトルが含まれていることに注意してください。これも適切に保持されています。

コンフィグレーション設定の追加

ユーザーが既知のイメージホストのリストを提供できるようにしたいとします。
これらのホストの画像を指すimgタグはインライン化できますが、他の画像は外部リンクである必要があります。
もちろん、内部(相対)リンクの既存の動作を維持したいと考えています。
まず、Extensionサブクラスに構成オプションを追加する必要があります。

class ImageExtension(Extension):
    def __init__(self, **kwargs):
        # Define a config with defaults
        self.config = {'hosts' : [[], 'List of approved hosts']}
        super(ImageExtension, self).__init__(**kwargs)

デフォルトで空のリストになるhostsコンフィグレーション設定を定義しました。
次に、extendMarkdownメソッドでそのオプションをtreeprocessorに渡す必要があります。

def extendMarkdown(self, md):
    # Pass host to the treeprocessor
    md.treeprocessors.register(InlineImageProcessor(md, hosts=self.getConfig('hosts')), 'inlineimageprocessor', 15)

次に、新しい設定を受け入れるようにtreeprocessorを変更する必要があります。

class InlineImageProcessor(Treeprocessor):
    def __init__(self, md, hosts):
        self.md = md
        # Assign the setting to the hosts attribute of the class instance
        self.hosts = hosts

次に、設定を使用してURLをテストするメソッドを追加できます。

from urllib.parse import urlparse

class InlineImageProcessor(Treeprocessor):
    ...
    def is_unknown_host(self, url):
        url = urlparse(url)
        # Return False if network location is empty or an known host
        return url.netloc and url.netloc not in self.hosts

最後に、runメソッドのif attrib['src'].startswith('http'):行をif self.is_unknown_host(attrib['src']):に置き換えることで、testメソッドを利用できます。

Test 2

最終結果は次のようになります。

ImageExtension.py

from markdown.treeprocessors import Treeprocessor
from markdown.extensions import Extension
from urllib.parse import urlparse


class InlineImageProcessor(Treeprocessor):
    def __init__(self, md, hosts):
        self.md = md
        self.hosts = hosts

    def is_unknown_host(self, url):
        url = urlparse(url)
        return url.netloc and url.netloc not in self.hosts

    def run(self, root):
        for element in root.iter('img'):
            attrib = element.attrib
            if self.is_unknown_host(attrib['src']):
                tail = element.tail
                element.clear()
                element.tag = 'a'
                element.set('href', attrib.pop('src'))
                element.text = attrib.pop('alt')
                element.tail = tail
                for k, v in attrib.items():
                    element.set(k, v)


class ImageExtension(Extension):
    def __init__(self, **kwargs):
        self.config = {'hosts' : [[], 'List of approved hosts']}
        super(ImageExtension, self).__init__(**kwargs)

    def extendMarkdown(self, md):
        md.treeprocessors.register(InlineImageProcessor(md, hosts=self.getConfig('hosts')), 'inlineimageprocessor', 15)

テストしてみましょう:

Test.py

import markdown

input = """
![a local image](/path/to/image.jpg)

![a remote image](http://example.com/image.jpg)

![an excluded remote image](http://exclude.com/image.jpg)
"""

from ImageExtension import ImageExtension
html = markdown.markdown(input, extensions=[ImageExtension(hosts=['example.com'])])
print(html)

そして、python Test.pyを実行すると、次の出力が返されます。

<p><img alt="a local image" src="/path/to/image.jpg"/></p>
<p><img alt="a remote image" src="http://example.com/image.jpg"/></p>
<p><a href="http://exclude.com/image.jpg">an excluded remote image</a></p>

上記の拡張機能を配布用のパッケージにまとめることは、読者の演習として残されています。

ページのトップへ戻る