2018年1月6日土曜日

目次を動的に生成する方法

このブログでは記事本文から目次を動的に生成して記事の前に表示しています。 このブログで行っている目次の動的生成方法をご紹介します。

目次を動的に生成する前提条件

記事本文から目次を動的に生成するには、記事本文に仕掛けが必要です。 その仕掛けは、このブログの記事である 「HTMLの見出しタグに連番を振るCSSの書き方」 でご紹介したように、記事中の見出しとする文章をh4~h6のHTMLタグで括り、そのclass属性値にそれぞれ、foacs-chapter、foacs-section、foacs-paragraphの値を設定します。

下記のような記述が記事本文に必要で、hタグで括られた文章を使って目次を動的に生成します。

<h4 class="foacs-chapter">章の見出し01</h4>

<p>章の文章01</p>

<h5 class="foacs-section">節の見出し0101</h5>

<p>節の文章0101</p>

<h6 class="foacs-paragraph">項の見出し010101</h6>

<p>項の文章010101</p>

これからご紹介する動的に目次を生成する方法で対応しているのは「章(foacs-chapter)」「節(foacs-section)」「項(foacs-paragraph)」の3段階のみです。

ちなみに、「foacs」はこのブログの英語名(灰色スズメの足跡=footprints of ash color sparrow)の単語の頭文字をつなげたもので、Bloggerで元から使用されている他のクラス名と重複しないように使用している接頭辞です。

目次を動的に生成する仕掛け

目次を動的に生成する仕掛けはJavaScriptで作っています。 そのプログラムについてご説明します。

目次を動的に生成するJavaScript

目次を動的に生成するJavaScriptのプログラムは下記のとおりです。 下記の目次生成関数は記事本文中のh4タグのclass属性値「foacs-chapter(章)」、h5タグのclass属性値「foacs-section(節)」、h6タグのclass属性値「foacs-paragraph(項)」を探して目次を生成します。

// 目次生成関数。
function generateContents()
{
  // 記事の表示要素一覧を取得。
  // Bloggerは2017年10月の記事一覧のように複数の記事表示ができる。
  const postBodies = document.body.getElementsByClassName('post-body entry-content');

  // 記事ごとに目次を生成する。
  for (let postBody = 0; postBody < postBodies.length; postBody++)
  {
    // 記事内にfoacs-chapterをクラス属性値に持つ要素があれば目次を生成する。
    if (0 < postBodies[postBody].getElementsByClassName('foacs-chapter').length)
    {
      // 目次生成。
      let htmlContents = `<div class='foacs-contents'><ol>`;

      let chapter = 0, section = 0, paragraph = 0;

      for (let childPostBody of postBodies[postBody].children)
      {
        if (childPostBody.className == 'foacs-chapter')
        {
          if (paragraph != 0)
          {
            htmlContents += `</li></ol>`;
            paragraph = 0;
          }
          if (section != 0)
          {
            htmlContents += `</li></ol>`;
            section = 0;
          }
          if (chapter != 0)
          {
            htmlContents += `</li>`;
          }

          chapter++;
        }

        if (childPostBody.className == 'foacs-section')
        {
          if (paragraph != 0)
          {
            htmlContents += `</li></ol>`;
            paragraph = 0;
          }
          if (section != 0)
          {
            htmlContents += `</li>`;
          }

          if (section == 0)
          {
            htmlContents += `<ol>`;
          }

          section++;
        }

        if (childPostBody.className == 'foacs-paragraph')
        {
          if (paragraph != 0)
          {
            htmlContents += `</li>`;
          }

          if (paragraph == 0)
          {
            htmlContents += `<ol>`;
          }

          paragraph++;
        }

        if (childPostBody.className == 'foacs-chapter' || childPostBody.className == 'foacs-section' || childPostBody.className == 'foacs-paragraph')
        {
          childPostBody.id = `foacs-heading-${postBody}-${chapter}-${section}-${paragraph}`;
          htmlContents += `<li><a href='#foacs-heading-${postBody}-${chapter}-${section}-${paragraph}' title='${childPostBody.textContent}'>${childPostBody.textContent}</a>`;
        }
      }

      if (paragraph != 0)
      {
        htmlContents += `</li></ol>`;
        paragraph = 0;
      }
      if (section != 0)
      {
        htmlContents += `</li></ol>`;
        section = 0;
      }
      if (chapter != 0)
      {
        htmlContents += `</li></ol>`;
        chapter = 0;
      }

      htmlContents += `</div>`;

      for (let postHeader of postBodies[postBody].parentElement.getElementsByClassName('post-header'))
      {
        // 既存の目次を削除する。
        for (let foacsContents of postHeader.getElementsByClassName("foacs-contents"))
        {
          if (foacsContents.parentNode)
          {
            foacsContents.parentNode.removeChild(foacsContents);
          }
        }

        // 記事本文の前にある記事ヘッダー要素に生成した目次を追記する。
        postHeader.innerHTML += htmlContents;
      }
    }
  }
}

上記の目次生成関数をBloggerレイアウト用HTMLに挿入して保存しておくと、目次生成関数はBloggerのすべての記事に対して目次を生成して表示します。

Bloggerレイアウト用HTMLに目次生成関数を挿入して保存する

目次生成関数は下記のようにBloggerレイアウト用HTMLに挿入します。

Blogger設定画面で、

  1. [テーマ]の編集画面を表示

    Blogger設定画面
  2. [HTMLの編集]ボタンを押下

    Bloggerレイアウト用HTML
  3. テキストボックス中のBloggerレイアウト用HTMLのbodyタグの終了タグの直前にscriptタグがあるので、 そこに上記の目次生成関数と呼び出し処理である下記のコードを挿入します。
    document.addEventListener('DOMContentLoaded', generateContents());
    上記のコードは、HTML文書の読み込みが終了した時に目次生成関数generateContentsを呼び出すという意味です。
    Bloggerレイアウト用HTMLは下記のようになります。

    
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
          <div class='content-cap-bottom cap-bottom'>
            <div class='cap-left' />
            <div class='cap-right' />
          </div>
        </div>
      </div>
    
      <script type='text/javascript'>
        window.setTimeout(function () {
          document.body.className = document.body.className.replace(' loading ', '');
        }, 10);
    
        // Insert Begin by foacs
        //<![CDATA[
        document.addEventListener('DOMContentLoaded', generateContents());
    
    
        // 目次生成関数。
        function generateContents()
        {
          // 記事の表示要素一覧を取得。
          // Bloggerは2017年10月の記事一覧のように複数の記事表示ができる。
          const postBodies = document.body.getElementsByClassName('post-body entry-content');
    
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
                // 記事本文の前にある記事ヘッダー要素に生成した目次を追記する。
                postHeader.innerHTML += htmlContents;
              }
            }
          }
        }
        //]]>
        // Insert End by foacs
      </script>
    </body>
    
    <macro:includable id='sections' var='col'>
      <macro:if cond='data:col.num == 0'>
        <macro:else/>
        <b:section mexpr:class='data:col.class' mexpr:id='data:col.idPrefix + "-1"' preferred='yes' showaddelement='yes'
        />
    
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    
  4. [テーマを保存]ボタンを押下して、Bloggerレイアウト用HTMLをBloggerに保存する

目次生成関数が生成するHTML

目次生成関数は下記の記事本文のHTMLを読み込み、

<h4 class="foacs-chapter">章の見出し01</h4>

<p>章の文章01</p>

<h5 class="foacs-section">節の見出し0101</h5>

<p>節の文章0101</p>

<h6 class="foacs-paragraph">項の見出し010101</h6>

<p>項の文章010101</p>

記事本文のHTMLから下記の目次のHTMLを生成します。

<div class="foacs-contents">
<ol>
    <li>
        <a href="#foacs-heading-0-1-0-0" title="章の見出し01">章の見出し01</a>
        <ol>
            <li>
                <a href="#foacs-heading-0-1-1-0" title="節の見出し0101">節の見出し0101</a>
                <ol>
                    <li>
                        <a href="#foacs-heading-0-1-1-1" title="項の見出し010101">項の見出し010101</a>
                    </li>
                </ol>
            </li>
        </ol>
    </li>
</ol>
</div>

生成した目次にはaタグにより記事内の当該箇所にリンクが張ってあります。 目次生成関数は目次のリンク先の記事本文のhタグに移動できるように、記事本文のhタグにid属性と属性値を追加します。

目次を挿入する箇所

目次生成関数は生成した目次HTMLをBloggerが用意しているclass属性値がpost-headerのdivタグの子要素として挿入します。 目次生成関数はBlogger記事ページに目次HTMLを下記のように挿入します。 Chrome DevTools で確認してみてください。


<div class="post hentry uncustomized-post-template" itemprop="blogPost" itemscope="itemscope" itemtype="http://schema.org/BlogPosting">

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    <h3 class="post-title entry-title" itemprop="name">
        記事題名
    </h3>
    <div class="post-header">
        <div class="post-header-line-1"></div>
        ★目次生成関数が目次HTMLを挿入する箇所★
    </div>
    <div class="post-body entry-content" id="post-body-xxxxx" itemprop="description articleBody">

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
記事本文
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

        <div style="clear: both;"></div>
    </div>
    <div class="post-footer">

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    </div>
</div>

目次を修飾するCSS

目次を修飾するCSSは下記のとおりです。

/*
目次設定
*/
.foacs-contents
{
    border: 1px solid var(--foacs-foreground-color);
    font-size: medium;
    margin: 4em auto;
    padding: 2em;
}

.foacs-contents::before
{
    content: "目次";
    font-size: larger;
}

.foacs-contents > ol
{
    counter-reset: --foacs-chapter-counter;
    counter-reset: --foacs-section-counter;
    counter-reset: --foacs-paragraph-counter;

    list-style-type: none;
    padding: 0;
}

.foacs-contents li
{
    line-height: 2em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.foacs-contents > ol > li
{
    counter-increment: --foacs-chapter-counter;
    counter-reset: --foacs-section-counter;
}

.foacs-contents > ol > li::before
{
    content: counter(--foacs-chapter-counter) ".";
    padding-right: 0.5em;
}

.foacs-contents > ol > li > ol > li
{
    counter-increment: --foacs-section-counter;
    counter-reset: --foacs-paragraph-counter;
}

.foacs-contents > ol > li > ol > li::before
{
    content: counter(--foacs-chapter-counter) "." counter(--foacs-section-counter) ".";
    padding-right: 0.5em;
}

.foacs-contents > ol > li > ol > li > ol > li
{
    counter-increment: --foacs-paragraph-counter;
}

.foacs-contents > ol > li > ol > li > ol > li::before
{
    content: counter(--foacs-chapter-counter) "." counter(--foacs-section-counter) "." counter(--foacs-paragraph-counter) ".";
    padding-right: 0.5em;
}

上記のCSSは下記のようにBloggerに追加します。

Blogger設定画面で、

  1. [テーマ]の編集画面を表示

    Blogger設定画面
  2. [カスタマイズ]ボタンを押下

  3. Bloggerテーマデザイナーで[上級者向け]ー[CSSを追加]を選択

    BloggerテーマデザイナーCSSを追加
  4. テキストボックス内にカスタムCSSを追記する

  5. 画面右上の[ブログに適用]ボタンを押下して、カスタムCSSをBloggerに追加する

記事終わり