そんなにGeekじゃないエンジニアブログ

[WordPress]全記事に目次を自動追加するスクリプトを書いてみた

calendar

こんにちは。でんすけ(@notgeek_densuke)です。

皆さん、ブログに「目次」付けてますか?

あってもなくてもいいんですけど、
あった方が、読み手のこと考えたブログだな、という感じがしますよね。

でも、すで記事もたくさん書いてきたし、
いまから目次を作るのは大変なんだよなー、
という方もいらっしゃるはず。

そんなときのために。
目次を作るスクリプトを書いてみました。

スポンサーリンク

目次作成スクリプトの仕様

まず、前提として

・すべての記事に目次を追加する
・見出しタグ(h2)へのリンクを作成
・リンク先URLは、h2のidにする。(<a href=”#h2のid”>)
・見出しに「id」がない場合は「autoid_1」「autoid_2」のように連番のidを自動付与
・「一番最初の見出しタグ」の前に目次を設置

という仕様にすることにします。

目次作成スクリプトの実装

functions.phpに
こんな感じのコードを記述。

function insert_table_of_contents( $the_content ){
	if(is_single()) {  //投稿ページの場合
		$tag = '/^<h2.*?>(.+?)<\/h2>$/im'; //見出しタグの検索パターン
		if(preg_match_all($tag, $the_content, $tags)) { //本文中に見出しタグが含まれていれば
			$idpattern = '/id *\= *["\'](.+?)["\']/i'; //見出しタグにidが定義されているか検索するパターン
			$table_of_contents = '<div class="table_of_contents"><p class="title"><目次></p><ul>';
			$idnum = 1;
			$nest = 0;

			for($i = 0 ; $i < count($tags[0]) ; $i++){
				if( ! preg_match_all($idpattern, $tags[0][$i], $idstr) ){
					//見出しタグにidが定義されていない場合、「autoid_1」のようなidを自動設定
					$idstr[1][0] = 'autoid_'.$idnum++; 
					$the_content = preg_replace( "/".str_replace('/', '\/' ,$tags[0][$i])."/", preg_replace('/(^<h2)/i', '${1} id="' . $idstr[1][0] . '" ' , $tags[0][$i]), $the_content,1);
				}
				//見出しへのリンクを目次に追加。<li>でリスト形式に。
				$table_of_contents .= '<li><a href="#' . $idstr[1][0] . '">' . $tags[1][$i] .'</a>';
			}

			$table_of_contents .= '</ul></div>'; //目次の各タグを閉じる

			if($tags[0][0]){
				//作った目次を、1番目の見出しタグの直前に追加
				$the_content = preg_replace('/(^<h[2-6].*?>.+?<\/h[2-6]>$)/im', $table_of_contents.'${1}', $the_content,1);
			}
		}
	}
	return $the_content;
}

// 記事表示時に「insert_table_of_contents()」を実行する
add_filter('the_content','insert_table_of_contents');

ざっくりした処理の流れはコメントの通りですが、
・記事本文から見出しタグ(h2)を探す
・h2にidがなかったら自動付与
・h2へのリンクを、目次に追加
・それを繰り返して目次を作ったら、最初の見出しの直前に目次を設置
という感じです。

ハイライトした14行目が異様にややこしくなったんですが、
「全く同じタイトルを付けた見出し」
というのを回避するためにこんな書き方になりました。
あんまりないケースだとは思うのですが、一応。

で、あとCSSも必要。
CSSは、ざっくりだけどこんな感じ。

div.table_of_contents{
	border:solid 1px #000000;
	background-color:#f3f3f3;
	max-width:300px;
	margin:20px auto;
}

div.table_of_contents p.title{
	padding:10px 0;
	text-align:center;
}

div.table_of_contents p,
div.table_of_contents ul,
div.table_of_contents li
{
	margin:0;
}

div.table_of_contents ul li ul {
    margin-left: -10px;
}

これらをそれぞれ、
functions.phpと
style.cssに書けば
こんな見た目の目次が各記事に表示されるようになるはず。

デモなので、リンク先URLは#にしてます。

<目次>

h2以外の見出しも目次に

で、先ほどのコードだと、h2だけを対象にして目次にしていましたが、
その他の見出し<h3>~<h6>も目次に入れるとすると、
こんな感じになります。

変更点は、ハイライトしたところ。

function insert_table_of_contents( $the_content ){
	if(is_single()) {  //投稿ページの場合
		$tag = '/^<h[2-6].*?>(.+?)<\/h[2-6]>$/im'; //見出しタグの検索パターン
		if(preg_match_all($tag, $the_content, $tags)) { //本文中に見出しタグが含まれていれば
			$idpattern = '/id *\= *["\'](.+?)["\']/i'; //見出しタグにidが定義されているか検索するパターン
			$table_of_contents = '<div class="table_of_contents"><p class="title"><目次></p><ul>';
			$idnum = 1;
			$nest = 0;

			for($i = 0 ; $i < count($tags[0]) ; $i++){
				if( ! preg_match_all($idpattern, $tags[0][$i], $idstr) ){
					//見出しタグにidが定義されていない場合、「autoid_1」のようなidを自動設定
					$idstr[1][0] = 'autoid_'.$idnum++; 
					$the_content = preg_replace( "/".str_replace('/', '\/' ,$tags[0][$i])."/", preg_replace('/(^<h[2-6])/i', '${1} id="' . $idstr[1][0] . '" ' , $tags[0][$i]), $the_content,1);
				}
				//見出しへのリンクを目次に追加。<li>でリスト形式に。
				$table_of_contents .= '<li><a href="#' . $idstr[1][0] . '">' . $tags[1][$i] .'</a>';
			}

			$table_of_contents .= '</ul></div>'; //目次の各タグを閉じる

			if($tags[0][0]){
				//作った目次を、1番目の見出しタグの直前に追加
				$the_content = preg_replace('/(^<h[2-6].*?>.+?<\/h[2-6]>$)/im', $table_of_contents.'${1}', $the_content,1);
			}
		}
	}
	return $the_content;
}
add_filter('the_content','insert_table_of_contents');

正規表現を変えただけです。
例えば、h4までしか要らない、という場合は、

$tag = '/^<h[2-4].*?>(.+?)<\/h[2-4]>$/im'; //見出しタグの検索パターン

みたいに変えられます。

これで、こんな感じになります。

見出しタグのネストを目次に反映

見出しタグのネスト構造を意識してブログを書かれている場合は
目次もネストしたいかもしれない。

そんなことも思ったので、
見出しタグのネスト構造を反映した目次も作ってみました。

function insert_table_of_contents( $the_content ){
	if(is_single()) {  //投稿ページの場合
		$tag = '/^<h[2-6].*?>(.+?)<\/h[2-6]>$/im'; //見出しタグの検索パターン
		if(preg_match_all($tag, $the_content, $tags)) { //本文中に見出しタグが含まれていれば
			$idpattern = '/id *\= *["\'](.+?)["\']/i'; //見出しタグにidが定義されているか検索するパターン
			$table_of_contents = '<div class="table_of_contents"><p class="title"><目次></p><ul>';
			$idnum = 1;
			$nest = 0;
			$nestTag = array();
			for($i = 0 ; $i < count($tags[0]) ; $i++){
				if( ! preg_match_all($idpattern, $tags[0][$i], $idstr) ){
					//見出しタグにidが定義されていない場合、「autoid_1」のようなidを自動設定
					$idstr[1][0] = 'autoid_'.$idnum++; 
					$the_content = preg_replace( "/".str_replace('/', '\/' ,$tags[0][$i])."/", preg_replace('/(^<h[2-6])/i', '${1} id="' . $idstr[1][0] . '" ' , $tags[0][$i]), $the_content,1);
				}
				//見出しへのリンクを目次に追加。<li>でリスト形式に。
				$table_of_contents .= '<li><a href="#' . $idstr[1][0] . '">' . $tags[1][$i] .'</a>';

				//見出しのネスト対応
				if($i+1 >= count($tags[0])){
					$table_of_contents .= '</li>';
				}
				else{
					preg_match_all('/^<h([2-6])/i' , $tags[0][$i] , $match1);
					preg_match_all('/^<h([2-6])/i' , $tags[0][$i+1], $match2);
					if($match1[1][0] < $match2[1][0]){
						$table_of_contents .= '<ul>';
						$nestTag[] = $match1[1][0];
						$nest++;
					}
					else if($match1[1][0] == $match2[1][0]){
						$table_of_contents .= '</li>';
					}
					else{
						while( count($nestTag) > 0 && $nestTag[count($nestTag)-1] >= $match2[1][0]){
							$table_of_contents .= '</li></ul>';
							array_splice($nestTag,count($nestTag)-1,1);
							$nest--;
						}
						$table_of_contents .= '</li>';
					}
				}
			}

			//入れ子のまま終わった時<ul>を閉じる
			for(; $nest > 0 ; $nest--){
				$table_of_contents .= '</ul></li>';
			}

			$table_of_contents .= '</ul></div>'; //目次の各タグを閉じる

			if($tags[0][0]){
				//作った目次を、1番目の見出しタグの直前に追加
				$the_content = preg_replace('/(^<h[2-6].*?>.+?<\/h[2-6]>$)/im', $table_of_contents.'${1}', $the_content,1);
			}
		}
	}
	return $the_content;
}
add_filter('the_content','insert_table_of_contents');

なんか結構複雑になった・・・
もうちょっとカッコよく書けたのかなぁ。
一応動いてるっぽいのでこれで。

動作例はこんな感じ。

<目次>

まとめ

ということで、ブログに目次を自動追加してみよう、というときのレシピ例でした。

add_filter(‘the_content’,関数名)
で、記事表示時に実行する関数を呼び出せるので
その関数内で目次生成すればよいです。

それではまたー。

この記事をシェアする

コメント

コメントはありません。

down コメントを残す




CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください