ことれいのもり

ParsedownのHTMLにCSSクラスをつける拡張クラスを作ってみた

はじめに

私が運営しているこのサイトは、記事内容をデータベースにMarkdown形式で保存しています。

そして、ページを表示するときにPHPのライブラリ「Parsedown」を使ってHTMLコードに変換しています。

しかし、Parsedownで変換されたHTMLにはタグにCSSクラス名がついていません。

CSSを適用させたい場合は、変換後のHTMLに対してクラスを追加する処理が必要になります。

今回はそのための拡張クラスを作ってみました。

前提

言語

拡張クラスの考え方

まず、ParsedownでMarkdown形式をHTMLに変換する例を見てみましょう。

$Parsedown = new Parsedown();

$markdown = '## タイトル\n\nここに記事内容が書かれます。';
$html = $Parsedown->text($markdown);

echo $html;
<h2>タイトル</h2>
<p>ここに記事内容が書かれます。</p>

ここで、

タグに対してCSSを適用したい場合、クラスを追加する必要があります。

Parsedown自体にはクラスを追加する機能がないため、変換後のHTMLを正規表現で加工します。

例えば、以下のような関数を使って、全ての

タグにクラスを追加できます。


  /**
   * h2タグにクラスをつける
   *
   * @param string $html HTMLに変換された文字列
   * @return void
   */
  private function addClassToH2($html)
  {
    $pattern = '/(<h2)([^>]*>)/';
    $replacement = '$1 class="underline-with-background"$2';
    return preg_replace($pattern, $replacement, $html);
  }

これを使うと、次のように変換されます。

<h2 class="underline-with-background">タイトル</h2>

これでCSSで自由に見た目を調整できるようになります。

拡張クラスの全体像

実際に私が使っている拡張クラスでは、以下のような処理を行なっています。

  • h2やh3タグにクラスを付与
  • 各見出し(

)と、それに続く本文を
で囲む
  • コードブロック(
    )を別の
    で囲んで装飾しやすくする
  • 通常の段落も

    タグで明示的に囲む

  • このようにすることで、Parsedownによって生成されたHTMLコードが見やすくなったりCSSの反映がしやすくなります。 以下に、実際の拡張クラスのコードを載せておきます。

    GitHub Gistにもコードを載せていますので、コピペしたい方などはこちらをご覧ください。 →GitHub Gistで見る

    <?php
    
    class CustomParsedown extends Parsedown
    {
      public function text($text)
      {
        // 親のメソッドでマークダウンをHTMLに変換
        $html = parent::text($text); 
    
        $html = $this->wrapParagraphs($html);
        $html = $this->wrapHeadingWithDiv($html);
        $html = $this->wrapCodeWithDiv($html);
        $html = $this->addClassToH2($html);
        $html = $this->addClassToH3($html);
        $html = $this->addClassToPre($html);
    
        return $html;
      }
    
      /**
       * <h2>~<h6>タグから次のh系タグまでのコンテンツを囲む
       *
       * @param string $html HTMLに変換された文字列
       * @return void
       */
      private function wrapHeadingWithDiv($html)
      {
        $pattern = '/(<h[2-6][^>]*>.*?<\/h[2-6]>)(.*?)(?=<h[2-6]|$)/s';  // h2~h6タグが基準
        $replacement = '<div class="article__content">$1$2</div>';
        return preg_replace($pattern, $replacement, $html);
      }
    
      /**
       * <pre>~</pre>のコードブロック間を<div>で囲む
       *
       * @param string $html HTMLに変換された文字列
       * @return void
       */
      private function wrapCodeWithDiv($html)
      {
        $pattern = '/(<pre[^>]*>.*?<\/pre>)/s';
        $replacement = '<div class="code-block">$1</div>';
        return preg_replace($pattern, $replacement, $html);
      }
    
      /**
       * h2タグにクラスをつける
       *
       * @param string $html HTMLに変換された文字列
       * @return void
       */
      private function addClassToH2($html)
      {
        $pattern = '/(<h2)([^>]*>)/';
        $replacement = '$1 class="underline-with-background"$2';
        return preg_replace($pattern, $replacement, $html);
      }
    
      /**
       * h3タグにクラスをつける
       *
       * @param string $html HTMLに変換された文字列
       * @return void
       */
      private function addClassToH3($html)
      {
        $pattern = '/(<h3)([^>]*>)/';
        $replacement = '$1 class="orange-circle"$2';
        return preg_replace($pattern, $replacement, $html);
      }
    
      private function addClassToPre($html)
      {
        $pattern = '/(<pre)([^>]*>)/';
        $replacement = '$1 class="line-numbers" data-copy="true"$2';
        return preg_replace($pattern, $replacement, $html);
      }
    
      /**
       * 段落内を<p>タグで囲む
       *
       * @param string $html
       * @return void
       */
      private function wrapParagraphs($html)
      {
        // 文の中身をマッチさせる
        $html = preg_replace_callback('/([^\n\r]+(\n|\r\n)?)/', function ($matches) use (&$isInsidePre) {
          $line = $matches[0];
    
          // 空行の場合は何も返さない
          if ($line === '') {
            return '';
          }
    
          // <pre>タグ内でのみ<p>タグはつけない
          $preHandled = $this->handlePreTags($line, $isInsidePre);
          if ($preHandled !== null) {
            return $preHandled;
          }
    
          // 他のHTMLタグがある場合はそのまま返す
          if ($this->hasOtherTags($line)) {
            return $line . "\n";
          }
    
          // 既に<p></p>で囲まれているときはそのまま返す
          if ($this->isWrappedInPTags($line)) {
            return $line . "\n";
          }
    
          // <p>タグが一切ついていないときは両方追加
          if (!$this->hasPTags($line)) {
            return '<p>' . trim($matches[1]) . '</p>' . "\n";
          }
    
          // <p>のみないときは追加
          if ($this->hasClosingPTag($line)) {
            return '<p>' . trim($matches[1]);
          }
    
          // </p>のみないときは追加
          if (!$this->hasClosingPTag($line)){
            return trim($matches[1]) . '</p>' . "\n";
          }
    
          return $line;
        }, $html);
    
        return $html;
      }
    
      /**
       * 先頭に<p>タグ以外のタグがあるか判定
       *
       * @param string $line 
       * @return boolean
       */
      private function hasOtherTags($line) {
        // 先頭に<p>
        return substr($line, 0, 1) === '<' && substr($line, 1, 1) !== 'p';
      }
    
      /**
       * <pre>タグ内部の処理
       *
       * @param string $line
       * @param boolean $isInsidePre  <pre>タグ内部かどうか
       * @return boolean
       */
      private function handlePreTags($line, &$isInsidePre)
      {
        // <pre>タグの開始を検知
        if (strpos($line, '<pre') !== false) {
          $isInsidePre = true;
          return $line;
        }
    
        // <pre>タグの終了を検知
        if (strpos($line, '</pre>') !== false) {
          $isInsidePre = false;
          return $line;
        }
    
        if ($isInsidePre) {
          return $line;
        }
    
        return null;
      }
    
      /**
       * <p>タグで囲まれているか判定
       *
       * @param string $line
       * @return boolean
       */
      private function isWrappedInPTags($line)
      {
        return preg_match('/^<p>.*<\/p>$/s', $line);
      }
    
      /**
       * <p>と</p>の両方があるか判定
       *
       * @param string $line
       * @return boolean
       */
      private function hasPTags($line) {
        return strpos($line, '<p>') !== false || strpos($line, '</p>') !== false;
      }
    
      /**
       * </p>で終わっているかを判定
       *
       * @param string $line
       * @return boolean
       */
      private function hasClosingPTag($line) {
        return substr($line, -4) === '</p>';
      }
    }

    おわりに

    Markdown形式からHTMLを生成してそのまま表示するだけだと、CSSが適用されていないものになります。

    今回紹介したように、Parsedownの変換後のHTMLに対して加工を行なうことで、見やすい文章が生成されます!

    「Markdown + HTML加工」で悩んでいる方の参考になれば幸いです。