トップへ戻る
BLOGS

WordPress 自動で目次を表示するプラグイン

WordPress 自動で目次を表示するプラグイン

こんにちは。 今回は自作のプラグインの作成の記録です
記事に対して自動で「目次」を出力するという内容で進めて行きます

なお、WordPressには多機能なプラグインが数多くあり、「WordPress目次プラグイン」などと検索すると簡単に目次を作成ししかも自由にカスタムできるようになります
しかし、プラグインを自分で作成することで非常に軽量かつ、他のプラグインとの競合も避けることができる

ということで開発工程を順を追って解説しましょう

目次プラグイン概要

本サイトのこの目次を作成します

  • コンテンツからh2~h5を自動で取得する
  • h2~h5に対してID属性を付与する
  • ID付与したh2~h5をコンテンツに戻す
  • 取得したタグをHTMLとして変数化する
  • コンテンツの最初のh2を取得する
  • 取得したh2の下に目次HTMLを出力する

以上の工程を一つづつ実行すれば目的の機能を実装できそうです

まずはプラグインの基礎としてプラグイン用の決り文句などをまとめて記しておこう
プラグインの基礎については以前の記事で紹介しているのでご参考に

まずは簡単に基礎コード投下

基礎コードテンプレート

<?php
/*
Plugin Name: Ojako Table Of Contents
Plugin URI: 
Description: おジャコ丸による目次生成プラグイン
Version: 1.0.0
Author:ojako
Author URI: https://ojako1012.com/
License: GPL2
*/
?>
<?php
/**
 * ライセンスを有効にするための設定
 * 使用法
 * 上で「License: GPL2」を指定したら下記の記述をコピーライトの西暦、作者メールアドレスを書き換えて使用する
 */
?>
<?php
/*  Copyright 2021 ojako-tabel-of-contents (email : youthfulday.8348@gmail.com)
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License, version 2, as
    published by the Free Software Foundation.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

//初期設定
class OjaTableContents {
  public function __construct() {
    register_activation_hook (__FILE__, array($this, 'oja_plugin_start'));

    register_deactivation_hook (__FILE__, array($this, 'oja_plugin_stop'));

    register_uninstall_hook (__FILE__, array($this, 'oja_plugin_end'));
  }
  public function oja_plugin_start() {
    //インストール時の設定;
    //データベース・ファイルの作成など
    
  }
  public function oja_plugin_stop(){
    //プラグイン、停止時の設定;
    //データの初期化など
  }
  public function oja_plugin_end(){
    //プラグイン、削除時の設定;
    //作成したデータベース・データ・ファイルの削除など
  }
}
//クラスオブジェクトの生成
new OjaTableContents;

require_once(dirname(__FILE__).'/lib/script.php');

簡単に説明
プラグインの作成はそれぞれ一意の値(名前)で関数やコンテンツネームを設定しなくてはいけないため、クラス変数にして自由に開発するためにクラス化してプラグインに必要なフックで処理を追加するようにします

the_contentにフックする

function oja_add_table_content( $content ) {
 //目次関数をここに書く
  return $content;
} 
add_filter( 'the_content', 'oja_add_table_content', 8, 1 );

今回は処理もそこまで複雑じゃないためここからはこのファイルに直接目次関数を書いていきます
続けてクラス変数にメインの目次用関数を書いていきましょう

正規表現のHTMLタグを用意する

 if ( is_single() && in_the_loop() && is_main_query() ) {
    //h2 ~ h5を正規表現で表す
    $pattern = '/<h[2-5]>(.*?)<\/h[2-5]>/i';
    //$contentから要素を検索する
    preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER );
  
  } //if ( is_single() && in_the_loop() && is_main_query() )
  return $content;
} ////function oja_add_table_content( $content )

if ( is_single() && in_the_loop() && is_main_query()
先頭の条件分岐はWordPress関数で指定しています内容は

投稿記事かつループ中かつメインクエリ(基礎ループの問い合わせ)である
という内容です

$pattern = ‘/<h[2-5]>(.*?)<\/h[2-5]>/i’;

上記の部分は正規表現を用いてHTMLのh2~h5を表しています
[2-5]は2〜5のどれかにマッチ
(.*?)は自由な文字を複数回マッチつまりどんな内容のテキストでもマッチ
最後の/iは整数値であることを表します
\はエスケープする必要のある記号の前に記述します

正規表現はとても奥が深く難解であるためうまくまとめて記事にしたいですね

コンテンツからh2〜h5を取得する

preg_match_all
( 正規表現, 検索文字列, マッチした文字を格納する配列, オプション );

preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER );

この関数でthe_contentフックの引数$contentつまり記事内容から$pattern(先に変数化したタグ)を検索しPREG_SET_ORDER をオプションに指定することで連想配列として
$matchesに値が格納されます
後はこの$matchesから値を得ながら各工程を進めていきます

取得したタグをループで処理する

ということでループ処理に入ります
今回作成するプラグインの条件として

h2~h5要素が3つ以上の場合に目次を出力

とするのでさらにif文でif ( count( $matches ) >= 3 )とすることで
3件以上の条件となります

またループ処理する際に使用する変数もループ前に定義しておきます

  if ( count( $matches ) >= 3 ) {
      // 目次出力に使用する変数
	$toc = '<p>この記事の目録</p>
               <ol class="postin-list">';
	// 目次の階層の判断に使用する変数
	$hierarchy = NULL;
	// ループ回数を数える変数
	$i = 0;
      foreach ( $matches as $element) {
        $i++;
        $id = 'toc_chapter'. $i;
        $chapter = preg_replace( '/<(.+?)>(.+?)<\/(.+?)>/',  '<$1 id="' . $id . '">$2</$3>', $element[0] );
        // idを挿入したタグでcontentを置換する
        $content = preg_replace( $pattern, $chapter, $content, 1);

ループで使用する変数の定義

// 目次出力に使用する変数
$toc = ‘<p>この記事の目録</p>
     <ol class=”postin-list”>’;

この<ol>タグから出力します
外側のタグであるためループ外で定義し閉じタグもループの外となります

// 目次の階層の判断に使用する変数
$hierarchy = NULL;

この変数は出力する目次ループの処理中に現在の階層を判定するために使用します
分かりにくいと思うのでまた説明します

// ループ回数を数える変数
$i = 0;

そのままの意味です
ループしながらその回数を利用しID属性として付与していくことでページ内リンクを作成する事ができます

取得したタグにID属性を付与する

 foreach ( $matches as $element) {
        $i++;
        $id = 'toc_chapter'. $i;
        $chapter = preg_replace( '/<(.+?)>(.+?)<\/(.+?)>/',  '<$1 id="' . $id . '">$2</$3>', $element[0] );

まずforeachループで$matchesを回していきます

$i++;でループするたびに数値を更新していき
$id = ‘toc_chapter’. $i;で指定のIDを設定して変数にしておきます

preg_replace( 検索文字列, 置換文字列, 対象 )

正規表現でグループ化した部分を$1~3で置換します
ここではidをタグに挿入しタグとして組み替えます

preg_match_allで作成した連想配列の中身は?

preg_match_allで取得してきた変数$matchesには

  • 0−0:タグ全文( <h2>なんちゃらかんちゃら</h2>)
  • 0−1:その中で次にヒットした文字列グループ
  • 1−0:次のタグ全文( <h3>うーちゃらくーちゃら</h3>)
  • 1−1:次にヒットした文字列グループ

というように構成されているため
foreachで回していく際は、
$element[0]とすることでタグを含む全文が、

$element[1]とすると本文が取得できます

preg_replace()で置換する対象として$element[0]を指定してその中からマッチする部分を置換します

‘/<(.+?)>(.+?)<\/(.+?)>/’

第一引数の正規表現でHTMLのタグを表現しています(.+?)の部分はグループであり内容としては「任意の一文字三文字繰り返す」表現です
このグループに対して左から置換対象を$マークで指定できます

‘<$1 id=”‘ . $id . ‘”>$2</$3>’

ここの部分がその指定で、こうすることで置換対象も正規表現(曖昧な表現)で表せれるのでこのグループの部分はそのままの文字列になります
そして変数化しておいたIDをタグの中へ入れて結果を変数にします

完成したタグを元のコンテンツに戻す

$content = preg_replace( $pattern, $chapter, $content, 1);

先程の正規表現パターンを使用し今度は記事全文である$contentから
正規表現部分を先程作ったID付きのタグで置換してあげれば
コンテンツのh2〜h5に対してIDが振られました

目次のHTMLを生成する

ここからはこのループを利用して目次となるHTMLコードを生成します

そのためにまず、現在ループ中のタグから閉じタグを判断する必要があります

コードです

switch ( true ) {
 case strpos( $element[0], '<h2' ) !== false:
  $tag_level = 2; break;
 case strpos( $element[0], '<h3' ) !== false:
  $tag_level = 3; break;
 case strpos( $element[0], '<h4' ) !== false:
  $tag_level = 4; break;
 case strpos( $element[0], '<h5' ) !== false:
  $tag_level = 5; break;
 }

strpos(検索対象, 検索文字列)

strpos()は検索用関数です

返り値は検索文字列の位置を整数で返すので
検索文字列が見つかった位置が0番目である場合は0が返ります

その場合「含まれている時」で条件設定したい場合は!== false:とする事で0番目の対応が可能です
switch文の条件はtrueにしてあるのでこれでそれぞれtrueになった場合に$tag_levelに現在ループ中のタグのレベル設定ができます

それではこのレベル設定と$hierarchy変数を利用してタグの階層をコントロールしましょう

現在のタグに応じて閉じタグを設定する

コードはこちら

// ループ一回目
 if( $i == 1 ) {
  $hierarchy = $tag_level;

   // 次のタグが下層となる場合
 } elseif ( $hierarchy < $tag_level ){
    $toc .= PHP_EOL .'<ol class="toc-h'. $tag_level .'">'. PHP_EOL;
    $hierarchy = $tag_level;

    // 次のタグが上層となる場合
 }elseif( $hierarchy > $tag_level ){
  $current_tag = $hierarchy - $tag_level;
   if( $current_tag > 1 ) {
    if ($tag_level == 4) $closing = str_repeat('</li>'. PHP_EOL .'</ol>', $current_tag - 1 );
    if ($tag_level == 5) $closing = str_repeat('</li>'. PHP_EOL .'</ol>', $current_tag - 2 );
 } else {
  $closing = str_repeat('</li>'. PHP_EOL .'</ol>', $current_tag );
  }
  $toc .= $closing . PHP_EOL .'</li>'. PHP_EOL;
  $hierarchy = $tag_level;

 // 同じ階層がそれぞれ連続する場合
  } elseif( $hierarchy === $tag_level ){
  $toc .= '</li>'. PHP_EOL;
   $hierarchy = $tag_level;
   }

  // HTMLとして生成する
  $title = $element[1];
  $toc .= '<li><a href="#' . $id . '">' . $title . '</a>';

長くなったのでブロックごとに解説していきます

まずは初回ループ

if( $i == 1 ) {
$hierarchy = $tag_level;

これは初回ループなので基本的には$hierarchyにh2が入ってきます


そしてこれは毎回であるが$hierarchyにはそのループの階層を代入した上で次のループへと回っていく


そして最後の方の記述で

$title = $element[1];
$toc .= ‘<li><a href=”#’ . $id . ‘”>’ . $title . ‘</a>’;

ここでHTMLタグを生成します

<a>タグにはhref属性に$idを指定しますので、クリックされた際に前の章でタグにIDを付与しておいたのでそこにジャンプするようにできます

次のループです

} elseif ( $hierarchy < $tag_level ){
$toc .= PHP_EOL .'<ol class=”toc-h’. $tag_level .'”>’. PHP_EOL;
 $hierarchy = $tag_level;

この条件は「現在のループタグが前のタグのレベルより低い」場合の条件である

その場合は新たにolタグを生成しそのタグのリストとします
それでは次の条件です

} elseif( $hierarchy > $tag_level ){

この条件は「前回のタグよりも今回のタグのレベルが高い」場合の条件である
この場合はまずどのレベル上層になるのか判断して閉じタグを出力する必要があります

$current_tag = $hierarchy – $tag_level;

まずは現在のタグレベルと前回のタグを引いてみます
そしてif( $current_tag > 1 ) {として2階層以上離れている場合には

if ($tag_level == 4) $closing = str_repeat(‘</li>’. PHP_EOL .'</ol>’, $current_tag – 1 );
if ($tag_level == 5) $closing = str_repeat(‘</li>’. PHP_EOL .'</ol>’, $current_tag – 2 );

str_repeat(文字列, 繰り返す回数)

返り値は繰り返された文字列です

これによりそれぞれのタグレベルに応じて閉じタグを出力する回数を変更しています

それ以外では

} else {
$closing = str_repeat(‘</li>’. PHP_EOL .'</ol>’, $current_tag );

で対応できます

そしてすべての条件下で最後に準備したタグ+閉じタグを出力します

$toc .= $closing . PHP_EOL .'</li>’. PHP_EOL;
$hierarchy = $tag_level;

最後のパターンです

} elseif( $hierarchy === $tag_level ){
 $toc .= ‘</li>’. PHP_EOL; $hierarchy = $tag_level;
}

ここでは「現在のタグレベルと前回のタグレベルが同じ場合」の条件です

ここまでで適切なタグを出し分けれたので最後には全体の閉じタグを出力します

} //foreach ( $matches as $element)
  // 閉じタグを出力
$toc .= str_repeat('</li></ol>', $tag_level - 1);

これですべての<hタグ>をolとしてリストとして変数化できました

作成したタグをHTMLにする

$index = '<nav class="postin-nav toc">
                  '. PHP_EOL . $toc .
                '</nav>'. PHP_EOL;

ここでは<nav>タグで変数化した目次HTMLをラップします

h2タグを探してコンテンツに挿入する

$h2 = '/<h2.*?>/i';
 if (preg_match($h2, $content, $h2s)) {
 $content = preg_replace($h2, $index . $h2s[0], $content, 1);
}

preg_match(/正規表現パターン/, 検索文字列, 代入する配列)

先に紹介したpreg_match_all()の単体バージョンです

一番最初にマッチした部分のみが変数として返ります
この関数自体の返り値はマッチしたら「1」、マッチしなければ「0」が返ります

したがってその返り値で条件分岐できます

後は同じように$contentから取得した最初のタグの前に変数化しておいた「目次」を置換します

生成した$contentをreturnします
今まで分岐してきた条件分岐の閉じタグも出力します

 } //if ( count( $matches ) > 3 )
} //if ( is_single() && in_the_loop() && is_main_query() )
return $content;

まとめです

以上で本サイトに使用している自作プラグイン
目次出力プラグインの解説でした
最後に完成した処理コード全文を貼っておきます

function oja_add_table_content( $content ) {
  if ( is_single() && in_the_loop() && is_main_query() ) {
    //h2 ~ h5を正規表現で表す
    $pattern = '/<h[2-5]>(.*?)<\/h[2-5]>/i';
    //$contentから要素を検索する
    preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER );
    // h2~h5要素が3つ以上の場合に目次を出力
    if ( count( $matches ) >= 3 ) {
      // 目次出力に使用する変数
			$toc = '<p>この記事の目録</p>
              <ol class="postin-list">';
			// 目次の階層の判断に使用する変数
			$hierarchy = NULL;
			// ループ回数を数える変数
			$i = 0;
      foreach ( $matches as $element) {
        $i++;
        $id = 'toc_chapter'. $i;
        // preg_replace( 検索文字列, 置換文字列, 対象 ) ※正規表現でグループ化した部分を$1~3で置換できる ここではidをタグに挿入
        $chapter = preg_replace( '/<(.+?)>(.+?)<\/(.+?)>/',  '<$1 id="' . $id . '">$2</$3>', $element[0] );
        // idを挿入したタグでcontentを置換する
        $content = preg_replace( $pattern, $chapter, $content, 1);

        // それぞれのタグによる階層構造の設定
        // strpos(検索対象, 検索文字列) 返り値:検索文字列の位置を整数で返す
        switch ( true ) {
          case strpos( $element[0], '<h2' ) !== false:
            $tag_level = 2; break;
          case strpos( $element[0], '<h3' ) !== false:
            $tag_level = 3; break;
          case strpos( $element[0], '<h4' ) !== false:
            $tag_level = 4; break;
          case strpos( $element[0], '<h5' ) !== false:
            $tag_level = 5; break;
        }

        // ループ一回目
        if( $i == 1 ) {
          $hierarchy = $tag_level;

        // 次のタグが下層となる場合
        } elseif ( $hierarchy < $tag_level ){
          $toc .= PHP_EOL .'<ol class="toc-h'. $tag_level .'">'. PHP_EOL;
          $hierarchy = $tag_level;

        // 次のタグが上層となる場合
        }	elseif( $hierarchy > $tag_level ){
          $current_tag = $hierarchy - $tag_level;
          if( $current_tag > 1 ) {
            // str_repeat(文字列, 繰り返す回数) ※返り値:繰り返された文字列
            if ($tag_level == 4) $closing = str_repeat('</li>'. PHP_EOL .'</ol>', $current_tag - 1 );
            if ($tag_level == 5) $closing = str_repeat('</li>'. PHP_EOL .'</ol>', $current_tag - 2 );
          } else {
            $closing = str_repeat('</li>'. PHP_EOL .'</ol>', $current_tag );
          }
          $toc .= $closing . PHP_EOL .'</li>'. PHP_EOL;
          $hierarchy = $tag_level;

        // 同じ階層がそれぞれ連続する場合
        } elseif( $hierarchy === $tag_level ){
          $toc .= '</li>'. PHP_EOL;
          $hierarchy = $tag_level;
        }

        // HTMLとして生成する
        $title = $element[1];
        $toc .= '<li><a href="#' . $id . '">' . $title . '</a>';
      } //foreach ( $matches as $element)
      // 閉じタグを出力
      $toc .= str_repeat('</li></ol>', $tag_level - 1);

      $index = '<nav class="postin-nav toc">
                  '. PHP_EOL . $toc .
                '</nav>'. PHP_EOL;

      $h2 = '/<h2.*?>/i';
      if (preg_match($h2, $content, $h2s)) {
        $content = preg_replace($h2, $index . $h2s[0], $content, 1);
      }
    } //if ( count( $matches ) > 3 )
  } //if ( is_single() && in_the_loop() && is_main_query() )
  return $content;
} ////function oja_add_table_content( $content )
add_filter( 'the_content', 'oja_add_table_content', 8, 1 );

以上です
お疲れさまでした

コメントをお待ちしております

お気軽にコメントをどうぞ。

CAPTCHA