menu-icon

PHP Markdownを拡張してみる

PHPのMarkdownパーサーであるPHP Markdownを導入して、自分好みに拡張してみました。

検証環境

検証に使った環境はPHP 8.0.1です。

$ php -v
PHP 8.0.1 (cli) (built: Jan 12 2021 01:57:17) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.1, Copyright (c) Zend Technologies

インストール

composerを使ってインストールします。

$ composer require michelf/php-markdown

試してみる

インストールできたら動作確認してみます。実行用のPHPファイル test.phpを用意します。

test.php
<?php

require 'vendor/autoload.php';

use Michelf\MarkdownExtra;

$my_text = <<<MYTEXT
# This is h1

hogehoge
MYTEXT;

$markdown = new MarkdownExtra();
echo $markdown->transform($my_text);

test.php を実行すると、HTMLに変換されて出力されます。

$ php test.php
<h1>This is h1</h1>

<p>hogehoge</p>

なお、利用できるパーサーには Michelf\MarkdownMichelf\MarkdownExtraの2つがありますが、特別な理由がなければ拡張されたmarkdownのパーサーであるMichelf\MarkdownExtraの方が使いやすいと思います(詳細はこちら)。

拡張してみる

markdownパーサーとして普通に使う分には、 Michelf\MarkdownExtraをそのまま使えば十分です。が、自分の使い方的には、FencedCodeブロックでファイル丸ごと書く場合も多いので、簡単な記述でファイル名を表示できるように拡張しようと思いました。

目標は、以下のような表示を簡単に記述できるようにすることです。

アイデア

FencedCodeブロックでファイル名を指定できるようにしたいので、Special Attributesを利用し、filename属性が指定された場合、 属性値を取り出してファイル名のHTML要素として出力するようにします。例えば以下の場合、hoge.txtがファイル名の要素として出力されるようにします。

``` [filename=hoge.txt]
fuga
```

ソース

アイデアを基に実装したものが以下になります。_doFencedCodeBlocks_callbackメソッドがFencedCodeブロックの変換処理を行う部分なので、これをオーバーライドする形になっています。

src/Markdown.php
<?php

namespace Src;

use Michelf\MarkdownExtra;

class Markdown extends MarkdownExtra
{
    /**
     * Callback to process fenced code blocks
     *
     * @param  array $matches
     * @return string
     */
    protected function _doFencedCodeBlocks_callback($matches)
    {
        $classname =& $matches[2];
        $attrs     =& $matches[3];
        $codeblock = $matches[4];

        $file_name = $this->pullFileNameFromAttrs($attrs);

        if ($this->code_block_content_func) {
            $codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname);
        } else {
            $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
        }

        $codeblock = preg_replace_callback('/^\n+/',
            array($this, '_doFencedCodeBlocks_newlines'), $codeblock);

        $classes = array();
        if ($classname !== "") {
            if ($classname[0] === '.') {
                $classname = substr($classname, 1);
            }
            $classes[] = $this->code_class_prefix . $classname;
        }
        $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes);
        $pre_attr_str  = $this->code_attr_on_pre ? $attr_str : '';
        $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;

        $file_name_element = $this->buildFileNameElement($file_name);
        $codeblock  = "<div class=\"code-block\">$file_name_element<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre></div>";

        return "\n\n".$this->hashBlock($codeblock)."\n\n";
    }

    /**
     * Pull file name from attributes
     *
     * @param  string $attrs
     * @return string
     */
    private function pullFileNameFromAttrs(string &$attrs): string
    {
        $matches = [];
        if (preg_match('/(^| +)filename=([^ ]+)/', $attrs, $matches) !== 1) {
            return '';
        }

        // remove filename attribute
        $attrs = trim(str_replace($matches[0], '', $attrs));

        return $matches[2];
    }

    /**
     * Build DOM element for file name
     *
     * @param  string $file_name
     * @return string
     */
    private function buildFileNameElement(string $file_name): string
    {
        if ($file_name === '') {
            return '';
        }

        return "<span class=\"filename\">{$file_name}</span>";
    }
}

実行結果

拡張したパーサーを試してみます。確認用に以下のファイルを用意しました。

test-extended.php
<?php

require 'vendor/autoload.php';

use Src\Markdown;

$my_text = <<<MYTEXT
# Code

This is `hello.php`.

``` {filename=hello.php}
<?php

echo 'Hello!'
```
MYTEXT;

$markdown = new Markdown();
?>
<html>
    <head>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <?= $markdown->transform($my_text); ?>
    </body>
</html>

実行してみると、filename属性に指定したファイル名が、 <span class="filename">hello.php</span> という要素として出力できていることがわかります。

$ php test-extended.php
<html>
    <head>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <h1>Code</h1>

<p>This is <code>hello.php</code>.</p>

<div class="code-block"><span class="filename">hello.php</span><pre><code>&lt;?php

echo 'Hello!'
</code></pre></div>
    </body>
</html>

そして適当なcss(style.css)を用意して、ブラウザで表示すれば以下のようにファイル名がそれっぽく見えます!

style.css
.code-block {
  position: relative;
  padding: 1rem;
  color: #efefef;
  background: #1e1e1e;
}

.filename {
  position: absolute;
  top: 0;
  color: #efefef;
  background: #6f6f6f;
  font-weight: 700;
}

まとめ

PHPのMarkdownパーサーであるPHP Markdownを自分好みに拡張してみました。PHP Markdownの拡張は自分で使うのに便利なのはもちろん、markdownの独自拡張の記法を簡単に取り扱えるので、特定のテーマに特化した情報共有サービスを作る場合にも利用できそうです。