Gatsby

Gatsbyのマークダウンで書いた記事で、気合いで目次を自動生成する

いつもご利用ありがとうございます。このブログは、広告費によって運営されています。

オススメ本
Web技術を勉強するなら、かなりオススメの雑誌です。毎月新しい発見があります。ついに最終号・・・、みなさん買いましょう!!
読んで損することはない名著。命名で悩むことが多い人はこの本がオススメです。

⇨ React 記事の目次はこちら

Gatsby のマークダウンで書いた記事に自動的に目次を追加します。プラグインはなし。気合いで書いたところがあります。

こちらの記事に進化形があります。

Gatsby で目次を自動生成する htmlAst を使った方法

はじめに

気合いで書いています。

イメージとしては、マークダウンで出力される HTML の文字列の中から

<h2>や<h3>

のタグ(文字列)を検索して、

あれこれ処理をするという流れになります。

つまり、コードブロックでプログラミングの記事を書いている人には微妙な記事かもしれません。

手順

  1. マークダウンで出力される文字列を取得する
  2. タグを探す
  3. 開始タグと終了タグが文字列の何番目にあるか取得する
  4. その間の文字列を取得する(目次のテキストの特定)
  5. タグに ID を付与する(ページ内リンクを設定するため)
  6. 後で生成する目次情報を連想配列にして用意
  7. 連想配列から目次を作る

完成形

const BlogPostTemplate = ({ data }) => {
  //dataにはmarkdownRemarkがgraphqlで取得されている
  var str = data.markdownRemark.html;
  var toc = "<p class='mb-4'>目次</p><ol>"; //最終的に作られる目次の開始タグ
  var array = []; //最終的に作られる目次の情報をぶっこむ

  //テキストを追加する関数str:おおもとの文字列、idx:場所(何文字目)、val:テキスト
  function strIns(str, idx, val) {
    var res = str.slice(0, idx) + val + str.slice(idx);
    return res;
  }

  //タグを探して、開始タグと終了タグの間のテキストを取得したりする(後述)
  function findText(ele) {
    var index = str.indexOf(`<${ele}>`);
    if (index === -1) return;
    var startIndex = index + 4;
    var lastIndex = str.indexOf(`</${ele}>`);
    var num = lastIndex - startIndex;
    var text = str.substr(startIndex, num);
    var object = {
      number: index,
      element: ele,
      text: text,
    };
    array.push(object);
    str = strIns(str, lastIndex + 4, ` `);
    str = strIns(str, index + 3, ` id="${text}"`);
  }


//10回ずつ検索しておけばいいだろう
for (var i = 0; i < 10; i++) {
  findText("h2");
  findText("h3");
}

//配列の順番をnumberの順に変える
const a = array.sort(function (first, second) {
  if (first.number > second.number) {
    return 1;
  } else if (first.number < second.number) {
    return -1;
  } else {
    return 0;
  }
});

//順番通りになった配列から目次のタグを作る,h2とh3でクラスを分けたいのでこうなっています。
a.forEach((v) => {
  var ele = "";
  if (v.element === "h2") {
    ele = `<li class="toc-h2"><a href="#${v.text}">${v.text}</a></li>`;
  } else if (v.element === "h3") {
    ele = `<li class="toc-h3 text-sm font-normal"><a href="#${v.text}">${v.text}</a></li>`;
  }
  toc += ele;
});

toc += "</ol>"; //終了タグ

//最後にtoc文字列をinnerHTMLで出力する
return (
  <div
    className="toc p-10 w-12/12 max-w-screen-sm m-auto font-bold bg-indigo-50 rounded-lg"
    dangerouslySetInnerHTML={{ __html: toc }}
  ></div>
);

テキストを探すところだけ解説

//タグを探して、開始タグと終了タグの間のテキストを取得したりする(後述)
function findText(ele) {
  var index = str.indexOf(`<${ele}>`);
  if (index === -1) return;
  var startIndex = index + 4;
  var lastIndex = str.indexOf(`</${ele}>`);
  var num = lastIndex - startIndex;
  var text = str.substr(startIndex, num);
  var object = {
    number: index,
    element: ele,
    text: text,
  };
  array.push(object);
  str = strIns(str, lastIndex + 4, ` `);
  str = strIns(str, index + 3, ` id="${text}"`);
}
var index = str.indexOf(`<${ele}>`);

開始タグが何文字目にあるか取得します。

if (index === -1) return;

文字列が見つからなかったら return してその処理は終了します。

var startIndex = index + 4;

開始タグの最後の文字は 4 文字後なので定義してます。

var lastIndex = str.indexOf(`</${ele}>`);

閉じタグの場所を取得します。

var num = lastIndex - startIndex;

目次に記載するテキストが何文字か取得します。

var text = str.substr(startIndex, num);

大元の文字列からテキストを取得します。(何文字目から、何文字を取得)

var object = {
  number: index,
  element: ele,
  text: text,
};
array.push(object);

何番目に、何タグで、なんというテキストを目次生成するか情報を配列に入れます。のちに number で数字が小さい順にします。

str = strIns(str, lastIndex + 4, ` `);
str = strIns(str, index + 3, ` id="${text}"`);

2回目以降の処理でも

<h2></h2>

を取得する処理をするので、1度処理したものを変形することによりスキップさせます(力技)

終了タグから書いている理由は、元の文字列から文字数が増えるから、「何文字目に」をずらさないように後ろからやっています(力技)

終了タグには ID とか付与する必要がないので、影響のない場所にスペースを放り込んでいます(力技)

最終的にこういう文字列になります。

<h2 id="テキストテキストテキスト"></h2 >

生成された ol や li タグにスタイルを当ててあげれば終了です。

まとめ

以上です。

何度も元の文字列を見に行ってるので、少なくとも一度検索した文字列は飛ばすような感じでかければ良かった思いましたが、いったんこれでいきます!ビルド時間やばくなったら変えたいと思います。

誰かの参考になれば幸いです。

それでは!!!

質問、誤記などあれば Twitter などでご指摘よろしくおねがいします!

また、以下の記事にもう少し簡潔にした内容があります。

Gatsby で目次を自動生成する htmlAst を使った方法

人気記事

PHP7.4 + Laravel6 のプロジェクトを AWS EC2 にデプロイする

関連記事

【ReactNative+CloudVision】「怒り顔採点アプリ」を作った