JavascriptによるDOM操作で目次を生成する方法

Javascriptで目次を生成

Javascriptで目次を生成

Javascriptで目次生成

2020年5月、JavascriptによるDOM操作でHTMLからヘッダータグを抽出して目次を生成しました。

HTMLから目次を生成

HTMLから目次を生成

DOM(Document Object Model)という「ドキュメントオブジェクトモデル」を使ってHTMLを操作します。
DOMは、マークアップされたリソース(Document)をリソース要素(Object)の木構造(Model)で表現して、プログラムからHTMLを自由に操作するための仕組みです。この仕組みを使って、表示されたWebサイトを動的に書き換えることができます。

HTMLからOL-LIで目次作成

HTMLからOL-LIで目次作成

HTMLからヘッダータグを抽出して目次を生成します。すべてのヘッダータグではなく、entry-contentクラス内の<h2>、<h3>、<h4>タグを抽出します。そして、これらのタグを順序付きリストの<ol>と<li>に変換して目次を生成します。

HTMLからタグで抽出

HTMLからタグで抽出

最初にDocument.getElementsByTagName()で<h2>、<h3>、<h4>タグを抽出することを考えましたが、別々に抽出するので階層構造が消えてしまいます。
すべてのhタグ(<h*>)を同時に抽出する方法はありません。

HTMLからクラス名で抽出

HTMLからクラス名で抽出

クラス名で抽出する方法があるので、抽出したい<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を下記に示します。

目次生成PAD make_toc

目次生成PAD make_toc


//======================================================
//  目次を生成
//
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);
    }
}
  1. 最初に関数setup_toc()で目次対象のHタグにクラス名を付けます。
  2. Hタグを管理するhtag_mngにより、目次対象のHタグを抽出します。
  3. 目次対象のHタグがあれば、
    1. 関数toc_create()で目次コンテナを生成してtoc_divとします。
    2. 関数toc_build()で目次を生成してtoc_divに追加します。
    3. insertTag()メソッドでHTML本体に目次を挿入します。

Hタグにクラス名を付与

関数setup_toc()のコードとPADを下記に示します。

目次生成PAD setup_tov

目次生成PAD setup_tov


//======================================================
//  目次対象の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を下記に示します。

目次生成PAD set_class_id

目次生成PAD set_class_id


//======================================================
//  タグにクラス名と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を下記に示します。

目次生成PAD toc_create

目次生成PAD toc_create


//======================================================
//  目次コンテナを生成
//
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を下記に示します。

目次生成PAD toc_build

目次生成PAD toc_build


//======================================================
//  目次の組み立て
//
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タグ管理オブジェクトです。

  1. 関数createElement(“ol”)で順序付きリストtoc_olに<ol>~</ol>を作成します。
  2. hobj.getHlv()で最初のHタグレベルを取得してh_lvに設定します。
  3. レベルキーk_lvに現在のレベルh_lvを設定して、同じレベルの間繰り返します。
    1. hobj.getHtx()でHタグテキストを取得してh_txに設定します。
    2. hobj.getHid()でHタグIDを取得してh_idに設定します。
    3. 関数createTextNode()で、toc_txにテキストノードを作成します。
    4. 関数createElement(“a”)で、toc_ahにリンクノードの<a>~</a>を作成します。
    5. 関数createElement(“li)で、toc_liにリスト項目の<li>~</li>を作成します。
    6. toc_ahにIDを設定してテキストノードtoc_txを子要素として追加します。
    7. toc_liにリンクノードtoc_ahを子要素として追加します。
    8. hobj.nextTag()で次のHタグに進めて、hobj.getHlv()でHタグレベルを取得してh_lvに設定します。
    9. レベルキーk_lvより現在のレベルh_lvが大きい場合は、以下の処理でそのレベルの処理を行います。
      1. 関数toc_build()を再帰呼び出ししてem_childに子供の要素を作成します。
      2. toc_liに子供の要素em_childを子要素として追加します。
      3. hobj.getHlv()でHタグレベルを取得してh_lvに設定します。
    10. 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を格納するフォルダを子テーマの下に作成します。

wordpressへの設置

wordpressへの設置

  •  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を組み込んだときと同じ要領です。

WordPress 素晴らしいシンタックス・ハイライト Prism.js

WordPress 特定のページのみ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の目次プラグインなど使う必要はありません。