WordPress プラグインなしで記事の見出しから目次を作成

WordPress で記事の見出しから目次を作成する方法を紹介します。

※ ちなみに、目次は英語で table of contents (【略】 TOC )です。

ググってみると既に多くのサイトで紹介されていますが、JavaScript(jQuery) による実装ばかりだったので、ここでは PHP による実装(目次を作成する部分)をしてみました。

PHP コード

functions.php などに下記のコードを記述します。

<?php

class Toc_Shortcode {

	private $add_script = false;
	private $atts = array();

	public function __construct() {
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
		add_shortcode( 'toc', array( $this, 'shortcode_content' ) );
		add_action( 'wp_footer', array( $this, 'add_script' ) );
	}

	function enqueue_scripts() {
		if ( !wp_script_is( 'jquery', 'done' ) ) {
			wp_enqueue_script( 'jquery' );
		}
	}

	public function shortcode_content( $atts ) {
		$this->atts = shortcode_atts( array(
			'id' => '',
			'class' => 'toc',
			'title' => '目次',
			'toggle' => 'true',
			'opentext' => '開く',
			'closetext' => '閉じる',
			'showcount' => 2,
			'depth' => 0,
			'toplevel' => 2,
			'targetclass' => 'entry-content',
			'offset' => '',
			'duration' => 'normal'
		), $atts );

		$content = get_the_content();

		$headers = array();
		preg_match_all( '/<([hH][1-6]).*?>(.*?)<\/[hH][1-6].*?>/u', $content, $headers );
		$header_count = count( $headers[0] );
		$counter = 0;
		$counters = array( 0, 0, 0, 0, 0, 0 );
		$current_depth = 0;
		$prev_depth = 0;
		$top_level = intval( $this->atts['toplevel'] );
		if ( $top_level < 1 ) $top_level = 1;
		if ( $top_level > 6 ) $top_level = 6;
		$this->atts['toplevel'] = $top_level;

		// 表示する階層数
		$max_depth = ( ( $this->atts['depth'] == 0 ) ? 6 : intval( $this->atts['depth'] ) );

		$toc_list = '';
		for ( $i = 0; $i < $header_count; $i++ ) {
			$depth = 0;
			switch ( strtolower( $headers[1][$i] ) ) {
				case 'h1': $depth = 1 - $top_level + 1; break;
				case 'h2': $depth = 2 - $top_level + 1; break;
				case 'h3': $depth = 3 - $top_level + 1; break;
				case 'h4': $depth = 4 - $top_level + 1; break;
				case 'h5': $depth = 5 - $top_level + 1; break;
				case 'h6': $depth = 6 - $top_level + 1; break;
			}
			if ( $depth >= 1 && $depth <= $max_depth ) {
				if ( $current_depth == $depth ) {
					$toc_list .= '</li>';
				}
				while ( $current_depth > $depth ) {
					$toc_list .= '</li></ul>';
					$current_depth--;
					$counters[$current_depth] = 0;
				}
				if ( $current_depth != $prev_depth ) {
					$toc_list .= '</li>';
				}
				if ( $current_depth < $depth ) {
					$toc_list .= '<ul' . ( ( $current_depth == 0 ) ? ' class="toc-list"' : '' ) . '>';
					$current_depth++;
				}
				$counters[$current_depth - 1]++;
				$number = $counters[0];
				for ( $j = 1; $j < $current_depth; $j++ ) {
					$number .= '.' . $counters[$j];
				}
				$counter++;
				$toc_list .= '<li><a href="#toc' . ($i + 1) . '"><span class="contentstable-number">' . $number . '</span> ' . $headers[2][$i] . '</a>';
				$prev_depth = $depth;
			}
		}
		while ( $current_depth >= 1 ) {
			$toc_list .= '</li></ul>';
			$current_depth--;
		}

		$html = '';
		if ( $counter >= $this->atts['showcount'] ) {
			$this->add_script = true;

			$toggle = '';
			if ( strtolower( $this->atts['toggle'] ) == 'true' ) {
				$toggle = ' <span class="toc-toggle">[<a class="internal" href="#">' . $this->atts['closetext'] . '</a>]</span>';
			}

			$html .= '<div' . ( $this->atts['id'] != '' ? ' id="' . $this->atts['id'] . '"' : '' ) . ' class="' . $this->atts['class'] . '">';
			$html .= '<p class="toc-title">' . $this->atts['title'] . $toggle . '</p>';
			$html .= $toc_list;
			$html .= '</div>' . "\n";
		}

		return $html;
	}

	public function add_script() {
		if ( !$this->add_script ) {
			return false;
		}

		$class = $this->atts['class'];
		$offset = is_numeric( $this->atts['offset'] ) ? (int)$this->atts['offset'] : - 1;
		$duration = is_numeric( $this->atts['duration'] ) ? (int)$this->atts['duration'] : '"' . $this->atts['duration'] . '"';
		$targetclass = trim( $this->atts['targetclass'] );
		if ( $targetclass == '' ) {
			$targetclass = get_post_type();
		}
		$targetclass = ".$targetclass :header";
		$opentext = $this->atts['opentext'];
		$closetext = $this->atts['closetext'];
		?>
<script type="text/javascript">
(function ($) {
  var offset = <?php echo $offset; ?>;
  var idCounter = 0;
  $("<?php echo $targetclass; ?>").each(function () {
    idCounter++;
    this.id = "toc" + idCounter;
  });
  $(".<?php echo $class; ?> a[href^='#']").click(function () {
    var href = $(this).attr("href");
    var target = $(href === "#" || href === "" ? "html" : href);
    var h = (offset === -1 ? $("#wpadminbar").height() + $(".navbar-fixed-top").height() : offset);
    var position = target.offset().top - h - 4;
    $("html, body").animate({scrollTop: position}, <?php echo $duration; ?>, "swing");
    return false;
  });
  $(".toc-toggle a").click(function () {
    var tocList = $(".toc-list");
    if (tocList.is(":hidden")) {
      tocList.show();
      $(this).text("<?php echo $closetext; ?>");
    } else {
      tocList.hide();
      $(this).text("<?php echo $opentext; ?>");
    }
  });
})(jQuery);
</script>
		<?php
	}

}

new Toc_Shortcode();

ショートコード

あとは、記事の表示したい場所に toc ショートコードを記述します。

[toc]

使用例:

[toc title="目次" depth="2"]

オプション

オプション デフォルト 説明
id “” ID です。
class “toc”

クラスです。

省略(”” を指定) することはできません。

title “目次” 目次のタイトルです。
toggle “true”

目次リストの開閉リンクを表示するかどうか。

表示する場合 “true”、表示しない場合は “false”。

opentext “開く” 開くリンクのテキストです。
closetext “閉じる” 閉じるリンクのテキストです。
showcount 2 目次を表示する見出し項目の数(以上)です。
depth 0

出力する階層数(階層レベルではありません)です。

0 の場合、すべての階層を表示します。

toplevel=”2″ depth=”4″ の場合、h2, h3, h4, h5 のヘッダーが対象となります。

toplevel 2 トップの階層のヘッダー(1 から 6)を指定します。
targetclass “entry-content” 目次を作成する対象のコンテンツのクラスを指定します。
offset “”

スクロール位置のオフセットです。

初期値は “” です。”” の場合は自動調整(*)となります。

* 通常は 0 ですが、上部固定エリア(管理バーやメニュー等)がある場合、その分の高さがセットされます。

duration “normal”

スクロール速度を指定します。

”slow”、”normal”、”fast”、もしくは完了までの時間をミリ秒単位で指定します。

CSS

.toc {
    width: auto;
    display: table;
    margin: 0 0 10px;
    padding: 10px;
    color: #333;
    word-break: break-all;
    word-wrap: break-word;
    border: #ccc solid 1px;
    border-radius: 3px;
    background-color: #fafafa;
}
.toc .toc-title {
    margin: 0;
    padding: 0;
    text-align: center;
    font-weight: bold;
}
.toc .toc-toggle {
    font-weight: normal;
    font-size: 90%;
}
.toc ul {
    list-style: none;
}
.toc .toc-list {
    margin: 0;
    padding: 0;
}

サンプルイメージ

toc

追記

2016年4月20日 jQuery バージョン 1.12 でエラーが発生する不具合を修正しました。

2017年3月9日 ヘッダーが順番に並んでいない場合に正しく目次を作成できない不具合を修正しました。

2017年3月10日 指定した階層よりも実際の階層が深い場合に正しく動作しませんでした。

2017年3月12日 コードを改修しました(修正はなし)。念のため jQuery をエンキュー(組み込み)する処理を追加しました。

コメント

  • 筥庭 蛙 より:

    石鷹さんが「WordPress プラグインなしで記事の見出しから目次を作成(https://xakuro.com/blog/wordpress/277)」で紹介されているPHPコードを使わせて頂いたのですが、どうしても目次の値がすべて「0」になってしまいます。目次の値を1,2,3…と順に並ぶようにしたいのですが、どうすればよいでしょうか。ご教授お願い致します。ちなみに、ショートコードは現在、[toc title=”目次” toplevel=”2″ depth=”2″]と入力しています(目次をh2とh3だけで作りたいから)。

    • 石鷹 より:

      すみません、コードに不具合がありました。
      コードを修正しましたので、もう一度お試しください。
      お手数をおかけして申し訳ございません。m(__)m

      • 筥庭 蛙 より:

        早速ご対応いただき誠にありがとうございました。ただ、先日と同じコード([toc title=”目次” toplevel=”2″ depth=”2″])を入力したところ、h2でしか目次が生成されず(下層のh3では目次が生成されない)、各目次リンクをクリックしても各見出しに移動しないという現象が起こりました。とくに急を要しているわけではないので、時間をかけてゆっくりと対処していただいてかまいません。こちらでもコードの不具合を修正できないか試みてみます。

    • 石鷹 より:

      度々すみません。指定した階層よりも実際の階層が深い場合に正しく動作しませんでした。
      とりあえず修正したもとをアップしましたが、週末にでもじっくり動作確認したいとおもっているので、お急ぎでなければ、しばらくお待ちください。
      それにしても改めてソースををみてみるとぐちゃぐちゃしていて汚いなと反省しています。

      • 筥庭 蛙 より:

        そんなことないですよ。石鷹さんのコードなら「PageSpeed Insights」 にも「The W3C Markup Validation Service」にも引っかからないので、とても助かっています。WordPressの目次作成プラグイン「Table of Contents」だとどちらか一方に必ず引っかかってしまうんですよ。気長に待っているので、どうか無理しないでくださいね。

    • 石鷹 より:

      一通りの動作確認してみましたが、正しく動作しているようです。
      ただし、ちょっとだけコードを(シンプルに)改修し、念のため jQuery を組み込む処理を追加したものをアップしたので、今回のものを使用してみてください。
      お手数かけます。m(__)m

コメントを残す

メールアドレスが公開されることはありません。

日本語でコメントを入力してください。