黙々とC#

"In a mad world of VBA, only the mad are sane" 『VBAという名の狂った世界で狂っているというのなら私の気は確かだ』

Nanoc4でRedcarpetのTOC(目次)生成機能が使いにくいので自前のFilterを書いた

f:id:d_ymkw:20170316234838j:plain

markdownのパーサにkramdownを使っていたのだけど、markdownファイルに含まれる生のHTMLコードの扱いに失敗することがあるので、redcarpetを使うことに

したのだけど、redcarpetはredcarpetでtoc生成に課題が出てきたので、自前のfilterを書いた。

目次

redcarpetのtocオプションの課題

で、Nanoc4にデフォルトで含まれているredcarpetのフィルタで、toc生成オプションを有効にしてみたのだけど、

  • tocの挿入位置が、bodyの先頭固定
  • tocのHTMLコード(ul,liタグ)にclassもidも付いていない
    • CSSでデザインをいじりづらい
    • 出力コードを変えるには、オリジナルのredcarpetのオプションを調べて、ガッツリfilterを書き換える必要あり
  • markdownにHTMLコードでベタにhタグを書いていると拾ってくれない?
    • 原因を調査する途中でもう投げたくなった

解決方法

HTMLコード(テンプレートファイル、もしくは、markdownファイル)中、{:toc} と書いた位置にtocを自動生成するfilterを書いた。

カスタムタグの定義ならredcarpetを拡張する方法もあるのだけど、今回は、markdownをパースしてさらにテンプレートを適用してHTMLに変換した後の状態に対して、tocを挿入したかったので、NanocのFilterとして実装する方法をとった。

書いたコード

jqueryを使ったjavascripのサンプルコードを元に、Nokogiriを用いて同じように書いてみた。

目次にする要素は、h2~h4に決め打ち。case文の分岐を書き換えれば変更可能。

class TocGenerator < Nanoc::Filter
  require 'Nokogiri'

  def run(content, params = {})
    createToc(content)
  end

  def createToc(content)
        id_count = 1
        toc_text = ''
        currentlevel = 0

        doc = Nokogiri::HTML.parse(content)
        doc.css('div.sc-content.c-container').css('h2, h3, h4').each do |h|
            h["id"] = 'chapter_' + id_count.to_s
            id_count += 1

            case h.name 
            when 'h2' then
                level = 1
            when 'h3' then
                level = 2
            when 'h4' then
                level = 3
            else
                level = 0
            end


            while currentlevel < level do
                toc_text += '<ul class="chapter">'
                currentlevel += 1
            end

            while currentlevel > level do
                toc_text += '</ul>'
                currentlevel -= 1
            end

            toc_text += '<li><a href="#' + h['id'] + '">' + h.content + "</a></li>\n";
        end

        while currentlevel > 0 do
            toc_text += '</ul>'
            currentlevel -= 1
        end

        toc_text = '<div id=toc><span id=toctitle>目次</span>' + toc_text + '</div>'
        newcontent = doc.to_html

        newcontent.gsub(/{:toc}/, toc_text)
  end
end

Nanoc::Filter.register "TocGenerator", :tocGenerator

RulesファイルでHTMLファイルに変換後に適用させて完成。(↓例)

compile '/**/*.md' do
  filter :erb
  filter :redcarpet, options: {tables: true}
  layout 'default.*'
  filter :tocGenerator
end