Javascriptで目次生成
2020年5月、JavascriptによるDOM操作でHTMLからヘッダータグを抽出して目次を生成しました。
DOM(Document Object Model)という「ドキュメントオブジェクトモデル」を使ってHTMLを操作します。
DOMは、マークアップされたリソース(Document)をリソース要素(Object)の木構造(Model)で表現して、プログラムからHTMLを自由に操作するための仕組みです。この仕組みを使って、表示されたWebサイトを動的に書き換えることができます。
HTMLからヘッダータグを抽出して目次を生成します。すべてのヘッダータグではなく、entry-contentクラス内の<h2>、<h3>、<h4>タグを抽出します。そして、これらのタグを順序付きリストの<ol>と<li>に変換して目次を生成します。
最初にDocument.getElementsByTagName()で<h2>、<h3>、<h4>タグを抽出することを考えましたが、別々に抽出するので階層構造が消えてしまいます。
すべてのhタグ(<h*>)を同時に抽出する方法はありません。
クラス名で抽出する方法があるので、抽出したい<h2>、<h3>、<h4>タグに同じクラス名を追加します。そして、Document.getElementsByClassName()により対象のhタグを抽出します。
entry-contentクラス内のhタグに限定して抽出するには、Document全体ではなくgetElementsByClassName()で「entry-contentクラス」を抽出すれば良いわけです。
目次生成のコード
Javascriptコード
下記に示すJavascriptをmakeToc.jsで作成します。
//=========================================================
// JavaScript 目次の作成
// [V00] 初版
//=========================================================
//======================================================
// Hタグ管理クラス
//
class htag_mng {
// class指定でHタグ取得
constructor(tClass) {
this.em_ix = 0;
this.htag_b = document.getElementsByClassName(tClass);
this.htag_n = this.htag_b.length;
}
// タグをHタグの直前に挿入
insertTag(em_tag) {
var em_ins = this.htag_b[0];
em_ins.parentNode.insertBefore(em_tag,em_ins);
}
// Hタグ終了
eof() {
return (this.em_ix >= this.htag_n) ? true : false;
}
// Hタグのレベル
getHlv() {
var h_lv = "";
if (this.eof()) {
h_lv = "H0";
} else {
h_lv = this.htag_b[this.em_ix].tagName;
}
return h_lv;
}
// Hタグのテキスト
getHtx() {
var h_tx = "";
if (! this.eof()) {
h_tx = this.htag_b[this.em_ix].textContent;
}
return h_tx;
}
// HタグのID
getHid() {
var h_id = "";
if (! this.eof()) {
h_id = this.htag_b[this.em_ix].getAttribute("id");
}
return h_id;
}
// 次のHタグ
nextTag() {
this.em_ix = this.em_ix+1;
}
}
//======================================================
// 目次コンテナを生成
//
function toc_create() {
// 目次 DIVを生成
var toc_div = document.createElement("div");
toc_div.className = 'toc_container';
// 目次 P タイトル
var toc_p = document.createElement("p");
toc_p.className = 'toc_title';
toc_p.textContent = 'この記事の目次';
//
toc_div.appendChild(toc_p);
return toc_div;
}
//======================================================
// 目次の組み立て
//
function toc_build(hobj) {
var toc_ol = document.createElement("ol");
var h_lv = hobj.getHlv();
var k_lv = h_lv;
while (k_lv == h_lv) {
var h_tx = hobj.getHtx();
var h_id = hobj.getHid();
//
var toc_tx = document.createTextNode(h_tx);
var toc_ah = document.createElement("a");
var toc_li = document.createElement("li");
toc_ah.href = "#"+h_id;
toc_ah.appendChild(toc_tx);
toc_li.appendChild(toc_ah);
//
hobj.nextTag();
h_lv = hobj.getHlv();
if (k_lv < h_lv) {
var em_child = toc_build(hobj);
toc_li.appendChild(em_child);
h_lv = hobj.getHlv();
}
toc_ol.appendChild(toc_li);
}
return toc_ol;
}
//======================================================
// 目次対象のHタグにクラスを付ける
//
function setup_toc(vClass) {
var doc_b = document.getElementsByClassName("entry-content");
var doc_n = doc_b.length;
for (var ix=0; ix < doc_n; ix++) {
set_class_id(doc_b[ix], "h2" ,vClass);
set_class_id(doc_b[ix], "h3" ,vClass);
set_class_id(doc_b[ix], "h4" ,vClass);
}
}
//======================================================
// タグにクラス名とIDを付ける
//
function set_class_id(Elm, hTag, vClass) {
var tag_b = Elm.getElementsByTagName(hTag);
var tag_n = tag_b.length;
var id_hd = "ID"+hTag+"-";
var id_no = 1000;
for (var ix=0; ix<tag_n; ix++) {
// Class
var oClass = tag_b[ix].className;
tag_b[ix].className = oClass + " " + vClass;
// ID
var orgID = tag_b[ix].getAttribute("id");
if (! orgID) {
var newID = id_hd + id_no.toString();
tag_b[ix].setAttribute("id",newID);
}
id_no++;
}
}
//======================================================
// 目次を生成
//
function make_toc() {
var vtoc = "toc-Class";
// 目次対象のHタグにクラスを付ける
setup_toc(vtoc);
// 目次対象のHタグを抽出
var vobj = new htag_mng(vtoc);
if (! vobj.eof()) {
// 目次コンテナを生成
var toc_div = toc_create();
// 目次を生成
var toc_ol = toc_build(vobj);
toc_div.appendChild(toc_ol);
// 目次を挿入
vobj.insertTag(toc_div);
}
}
//======================================================
// ロード終了時に目次を作成
//
//window.onload = make_toc;
document.addEventListener('DOMContentLoaded',make_toc, false);
//////////////////////////////////////////////////////////////////////////
cssコード
下記に示すcss定義をmakeToc.cssで作成します。
@charset "utf-8";
div.toc_container {
display: inline-block;
margin: 20px 30px 10px;
padding: 5px 20px;
border: 2px dimgray solid;
background-color: aliceblue;
}
div.toc_container p.toc_title {
margin: 0 15px 5px 15px;
padding: 5px;
text-align: left;
font-size: large;
font-weight: bold;
background-color: skyblue;
text-align: left;
}
div.toc_container ol
{
margin: 5px;
counter-reset : olcnt;
list-style-type: none;
}
div.toc_container li:before
{
counter-increment: olcnt;
content: counters(olcnt, "-") ". ";
font-weight: bold;
}
div.toc_container a {
text-decoration: none;
}
div.toc_container a:hover {
text-decoration: underline;
}
HTMLへの組み込み
目次を生成するHTMLコンテンツのヘッダー部に下記コードを埋め込みます。
<head>
<SCRIPT Language="JavaScript" src="makeToc.js"></SCRIPT>
<LINK rel="stylesheet" href="makeToc.css" type="text/css">
</head>
テスト用のHTML
テスト用のHTMLを下記に示します。
<body>
<h1>サイトのタイトル</h1>
<h2>サブタイトル</h2>
<div class="entry-content">
<h2>TITLE-A</h2>
<h2>TITLE-B</h2>
<h3>TITLE-B1</h3>
<h3>TITLE-B2</h3>
<h2>TITLE-C</h2>
</div>
</body>
目次生成makeToc.jsの解説
目次生成のメイン部
目次生成のメイン部の関数make_toc()のコードとPADを下記に示します。
//======================================================
// 目次を生成
//
function make_toc() {
var vtoc = "toc-Class";
// 目次対象のHタグにクラスを付ける
setup_toc(vtoc);
// 目次対象のHタグを抽出
var vobj = new htag_mng(vtoc);
if (! vobj.eof()) {
// 目次コンテナを生成
var toc_div = toc_create();
// 目次を生成
var toc_ol = toc_build(vobj);
toc_div.appendChild(toc_ol);
// 目次を挿入
vobj.insertTag(toc_div);
}
}
- 最初に関数setup_toc()で目次対象のHタグにクラス名を付けます。
- Hタグを管理するhtag_mngにより、目次対象のHタグを抽出します。
- 目次対象のHタグがあれば、
- 関数toc_create()で目次コンテナを生成してtoc_divとします。
- 関数toc_build()で目次を生成してtoc_divに追加します。
- insertTag()メソッドでHTML本体に目次を挿入します。
Hタグにクラス名を付与
関数setup_toc()のコードとPADを下記に示します。
//======================================================
// 目次対象のHタグにクラスを付ける
//
function setup_toc(vClass) {
var doc_b = document.getElementsByClassName("entry-content");
var doc_n = doc_b.length;
for (var ix=0; ix < doc_n; ix++) {
set_class_id(doc_b[ix], "h2" ,vClass);
set_class_id(doc_b[ix], "h3" ,vClass);
set_class_id(doc_b[ix], "h4" ,vClass);
}
}
仮引数vClassは、対象hタグに付けるクラス名です。
getElementsByClassName()で”entry-content”クラスを抽出し、<h2>、<h3>、<h4>タグのクラス名にvClassを追加します。
関数set_class_id()のコードとPADを下記に示します。
//======================================================
// タグにクラス名とIDを付ける
//
function set_class_id(Elm, hTag, vClass) {
var tag_b = Elm.getElementsByTagName(hTag);
var tag_n = tag_b.length;
var id_hd = "ID"+hTag+"-";
var id_no = 1000;
for (var ix=0; ix<tag_n; ix++) {
// Class
var oClass = tag_b[ix].className;
tag_b[ix].className = oClass + " " + vClass;
// ID
var orgID = tag_b[ix].getAttribute("id");
if (! orgID) {
var newID = id_hd + id_no.toString();
tag_b[ix].setAttribute("id",newID);
}
id_no++;
}
}
- 仮引数Elmは、HTMLの要素です。
- 仮引数hTagは、クラス名を付けるhタグです。
- 仮引数vClassは、対象hタグに付けるクラス名です。
Elm要素の中のhTagタグにクラス名vClassを追加します。
生成した目次からhタグへのリンクを作成するために、hタグにIDも同時に付記します。
IDは、「id=”IDh2-1000″」のようにhタグと4桁の連番で構成します。もし、すでにIDが付けられている場合は、そのIDを優先します。
目次コンテナを生成
関数toc_create()のコードとPADを下記に示します。
//======================================================
// 目次コンテナを生成
//
function toc_create() {
// 目次 DIVを生成
var toc_div = document.createElement("div");
toc_div.className = 'toc_container';
// 目次 P タイトル
var toc_p = document.createElement("p");
toc_p.className = 'toc_title';
toc_p.textContent = 'この記事の目次';
//
toc_div.appendChild(toc_p);
return toc_div;
}
- 関数createElement(“div”)で目次コンテナtoc_divに<div>~</div>を作成します。
- 関数createElement(“p”)でパラグラフtoc_pの<p>~</p>を作成して、タイトルを設定します。
- 関数appendChild()で、目次コンテナtoc_divにパラグラフtoc_pを子要素として追加します。
その結果、次のようなコードを生成します。
<div class="toc_container">
<p class="toc_title">この記事の目次</p>
</div>
目次の組み立て
関数toc_build()のコードとPADを下記に示します。
//======================================================
// 目次の組み立て
//
function toc_build(hobj) {
var toc_ol = document.createElement("ol");
var h_lv = hobj.getHlv();
var k_lv = h_lv;
while (k_lv == h_lv) {
var h_tx = hobj.getHtx();
var h_id = hobj.getHid();
//
var toc_tx = document.createTextNode(h_tx);
var toc_ah = document.createElement("a");
var toc_li = document.createElement("li");
toc_ah.href = "#"+h_id;
toc_ah.appendChild(toc_tx);
toc_li.appendChild(toc_ah);
//
hobj.nextTag();
h_lv = hobj.getHlv();
if (k_lv < h_lv) {
var em_child = toc_build(hobj);
toc_li.appendChild(em_child);
h_lv = hobj.getHlv();
}
toc_ol.appendChild(toc_li);
}
return toc_ol;
}
仮引数hobjは、Hタグ管理オブジェクトです。
- 関数createElement(“ol”)で順序付きリストtoc_olに<ol>~</ol>を作成します。
- hobj.getHlv()で最初のHタグレベルを取得してh_lvに設定します。
- レベルキーk_lvに現在のレベルh_lvを設定して、同じレベルの間繰り返します。
- hobj.getHtx()でHタグテキストを取得してh_txに設定します。
- hobj.getHid()でHタグIDを取得してh_idに設定します。
- 関数createTextNode()で、toc_txにテキストノードを作成します。
- 関数createElement(“a”)で、toc_ahにリンクノードの<a>~</a>を作成します。
- 関数createElement(“li)で、toc_liにリスト項目の<li>~</li>を作成します。
- toc_ahにIDを設定してテキストノードtoc_txを子要素として追加します。
- toc_liにリンクノードtoc_ahを子要素として追加します。
- hobj.nextTag()で次のHタグに進めて、hobj.getHlv()でHタグレベルを取得してh_lvに設定します。
- レベルキーk_lvより現在のレベルh_lvが大きい場合は、以下の処理でそのレベルの処理を行います。
- 関数toc_build()を再帰呼び出ししてem_childに子供の要素を作成します。
- toc_liに子供の要素em_childを子要素として追加します。
- hobj.getHlv()でHタグレベルを取得してh_lvに設定します。
- toc_olにリスト項目toc_liを子要素として追加します。
実行した結果、次のような目次を生成します。
<p class="toc_title">この記事の目次</p>
<ol>
<li><a href="#IDh2-1000">TITLE-A</a></li>
<li><a href="#IDh2-1001">TITLE-B</a>
<ol>
<li><a href="#IDh3-1000">TITLE-B1</a></li>
<li><a href="#IDh3-1001">TITLE-B2</a></li>
</ol>
</li>
<li><a href="#IDh2-1002">TITLE-C</a></li>
</ol>
目次コンテナtoc_divは、下記に示す図のような構造となっています。
目次の作成開始
目次を生成する関数make_toc()は、下記コードによりDOMツリーの構築が完了した時点で実行します。
document.addEventListener('DOMContentLoaded',make_toc, false);
目次生成をwordpressへ組み込み
目次生成makeTocをwordpressのhpb20-child子テーマに設置します。
makeTocのアップロード
cssとjsを格納するフォルダを子テーマの下に作成します。
- makeToc.jsは、「/wp-content/themes/hpb20-child/js」にアップロードします。
- makeToc.cssは、「/wp-content/themes/hpb20-child/css」にアップロードします。
WordPressに組み込み
テーマのための関数(functions.php)で、makeToc.cssとmakeToc.jsを組み込みます。
シンタックス・ハイライト Prism.jsを組み込んだときと同じ要領です。
すべのページに目次は不要なので、特定のページに組み込むこととします。
カスタムフィールドmake_tocが’yes’ のときに、目次を生成します。
function my_enqueue_scripts() {
…(中略)…
$make_toc = get_post_meta( get_queried_object_id(), 'make_toc', true);
if ( $make_toc == 'yes' ) {
makeToc_enqueue_scripts(); /* makeToc */
}
…(中略)…
}
/*=======================================================
* makeToc.js (makeToc.css + makeToc.js)
*=======================================================*/
function makeToc_enqueue_scripts() {
$theme_uri = get_stylesheet_directory_uri();
wp_enqueue_style(
'makeToc-css', $theme_uri . '/css/makeToc.css'
);
wp_enqueue_script(
'makeToc-js' , $theme_uri . '/js/makeToc.js',
array(), false, true
);
}
add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );
DOMは素晴らしい
DOMを使用すると動的にHTMLを書き換えることができとても便利です。
ヘッダータグを抽出して目次を生成できるので、wordpressの目次プラグインなど使う必要はありません。