Qiita記事の目次を自動生成するGroovyスクリプト

１ページの内容が長くなると、目次が欲しくなるので作ってみた。

__目次__

<ul>
  <li><a href="#1-1">Ver2</a>
    <ul>
      <li><a href="#2-1">変更点</a></li>
      <li><a href="#2-2">使い方</a>
        <ul>
          <li><a href="#3-1">ローカルのファイルを指定する</a></li>
          <li><a href="#3-2">コマンドラインから入力する</a></li>
        </ul>
      </li>
      <li><a href="#2-3">注意点</a></li>
      <li><a href="#2-4">実装</a></li>
    </ul>
  </li>
  <li><a href="#1-2">Ver1</a>
    <ul>
      <li><a href="#2-5">実装</a></li>
      <li><a href="#2-6">使い方</a></li>
      <li><a href="#2-7">参考</a></li>
    </ul>
  </li>
</ul>

#Ver2
##変更点
- ul タグを出力するようにしたことで、３段以上の見出しにも対応。
- 解析する記事の読み込みを、ネット越しではなくローカルからに変更（ファイル or コマンドライン）。

##使い方
スクリプトのファイル名は `script.groovy` という前提で説明。

###ローカルのファイルを指定する
```text:
>groovy script.groovy path/to/article.txt
```

第１引数に読み込む記事のパスを指定する。

###コマンドラインから入力する
```text:
>groovy script.groovy
記事のテキストをペーストしてください（"@end" で読み込みを終了）
#hoge
#fuga
##fuga-fugaa
###fuga-fugaa-fugaaa
##fuga-fugab
#piyo
##piyo-piyoo
@end
-----------------------------------------------------
__目次__

<ul>
  <li><a href="#1-1">hoge</a></li>
  <li><a href="#1-2">fuga</a>
    <ul>
      <li><a href="#2-1">fuga-fugaa</a>
        <ul>
          <li><a href="#3-1">fuga-fugaa-fugaaa</a></li>
        </ul>
      </li>
      <li><a href="#2-2">fuga-fugab</a></li>
    </ul>
  </li>
  <li><a href="#1-3">piyo</a>
    <ul>
      <li><a href="#2-3">piyo-piyoo</a></li>
    </ul>
  </li>
</ul>
```

解析したい記事を入力して最後に `@end` と入力すると、直前まで入力したテキストをもとに目次が生成される。

##注意点
見出しの文字列に Markdown の記法が使用されていると、それがそのまま出力されます。

```text:例
>groovy script.groovy
記事のテキストをペーストしてください（"@end" で読み込みを終了）
#`インラインコード`
##__強調__
@end
-----------------------------------------------------
__目次__

<ul>
  <li><a href="#1-1">`インラインコード`</a>
    <ul>
      <li><a href="#2-1">__強調__</a></li>
    </ul>
  </li>
</ul>
```

##実装
```groovy:
def article

if (existsCommandLineParameter()) {
    article = new Article(text: getTextFromFile())
} else {
    article = new Article(text: getTextFromConsole())
}

Index index = new Index()

article.eachHeadlines { level, anchorHref, title ->
    index.addNextHeadline(level, anchorHref, title)
}

println '-----------------------------------------------------'
println '__目次__'
println()
println index.root

////////////////////////////////////////////////////////////////////////////////////
boolean existsCommandLineParameter() {
    args.length != 0
}

String getTextFromConsole() {
    println '記事のテキストをペーストしてください（"@end" で読み込みを終了）'
    
    def reader = System.in.newReader()
    def sb = new StringBuilder()
    def line
    
    while ((line = reader.readLine()) != '@end') {
        sb << line << System.getProperty('line.separator')
    }
    
    sb.toString()
}

String getTextFromFile() {
    def file = new File(args[0])
    
    if (!file.exists() || !file.isFile()) {
        throw new IOException("指定したファイル (${args[0]}) が存在しません")
    }
    
    file.text
}

////////////////////////////////////////////////////////////////////////////////////
def class Article {
    def text
    
    def eachHeadlines(closure) {
        def numberParHeadlineLevel = [:]
        
        def inCodeBlock = false
        
        text.eachLine {
            if (inCodeBlock) {
                if (isEndOfCodeBlock(it)) {
                    inCodeBlock = false
                }
            } else {
                if (isStartOfCodeBlock(it)) {
                    inCodeBlock = true
                    
                } else if (Headline.isHeadline(it)) {
                    Headline headline = new Headline(text: it)
                    
                    def headlineLevel = headline.getLevel()
                    def title = headline.getTitle()
                    
                    countUpParHeadlineLevel(numberParHeadlineLevel, headlineLevel)
                    def href = "#${headlineLevel}-${numberParHeadlineLevel[headlineLevel]}"
                    
                    closure(headlineLevel, href, title)
                }
            }
        }
    }
    
    static def countUpParHeadlineLevel(map, headlineLevel) {
        if (map.containsKey(headlineLevel)) {
            map[headlineLevel] = map[headlineLevel] + 1
        } else {
            map[headlineLevel] = 1
        }
    }
    
    static def isStartOfCodeBlock(text) {
        text =~ /^```.*:.*/
    }
    
    static def isEndOfCodeBlock(text) {
        text =~ /^```$/
    }
    
    String toString() {
        this.text
    }
}

def class Headline {
    def text
    
    static def isHeadline(text) {
        text =~ /^#.*$/
    }
    
    def getLevel() {
        this.text.find(~/^#+/).length()
    }
    
    def getTitle() {
        this.text - ~/^#+/
    }
}

def class Index {
    def root = new Ul(indentSize: 0)
    def stack = new Stack()
    def currentLevel = 1
    
    def Index() {
        this.stack.push(this.root)
    }
    
    /**
     * 次の見出しを追加する。
     * @param nextLevel 次に追加する見出しのレベル
     * @param anchorHref ページ内の見出しに飛ぶための href 属性
     * @param title 見出しタイトル
     */
    def addNextHeadline(nextLevel, anchorHref, title) {
        def newHeadline = new Li(a: new A(href: anchorHref, text: title))
        
        if (this.currentLevel == nextLevel) {
            this.appendHeadline(newHeadline)
            
        } else if (this.currentLevel < nextLevel) {
            this.raiseHeadline(nextLevel, newHeadline)
            
        } else if (nextLevel < this.currentLevel) {
            this.lowerHeadline(nextLevel, newHeadline)
            
        }
    }
    
    def appendHeadline(li) {
        this.stack.peek().addLi(li)
    }
    
    def raiseHeadline(level, li) {
        Ul ul = new Ul(indentSize: level - 1)
        ul.addLi(li)
        this.stack.peek().lis.last().ul = ul
        this.stack.push(ul)
        this.currentLevel = level
    }
    
    def lowerHeadline(level, li) {
        (level..<this.currentLevel).each {
            this.stack.pop()
        }
        this.stack.peek().addLi(li)
        this.currentLevel = level
    }
}

def class Ul {
    static def LS = System.getProperty('line.separator')
    def lis = []
    def indentSize
    
    def addLi(li) {
        this.lis.add(li)
    }
    
    String toString() {
        def indent = '    ' * this.indentSize
        def sb = new StringBuilder()
        
        sb << indent << '<ul>' << LS
        lis.each {
            sb << indent << '  ' << it.toString(indent + '  ')
        }
        sb << indent << '</ul>' << LS
        
        sb.toString()
    }
}

def class Li {
    static def LS = System.getProperty('line.separator')
    def level
    def ul
    def a
    
    String toString(indent) {
        def sb = new StringBuilder()
        
        sb << '<li>' << this.a
        if (this.ul) {
            sb << LS << this.ul << indent
        }
        sb << '</li>' << LS
        
        sb.toString()
    }
}

def class A {
    def href
    def text
    
    String toString() {
        def escapedText = this.text.replace('<', '&lt;').replace('>', '&gt;')
        "<a href=\"${this.href}\">${escapedText}</a>"
    }
}
```

#Ver1

既に投稿されている記事が対象です。

##実装
```groovy:
import org.cyberneko.html.parsers.SAXParser

// インデックスを作成するページの URL を指定
def url = 'http://qiita.com/opengl-8080/items/6fb69cd2493e149cac3a'

def parser = new XmlSlurper(new SAXParser())
def html = parser.parse(url);

def hTags = html.'**'.grep { it.name() == 'A' && it.parent().name() =~ /H[0-9]/ && it.@href =~ /#[0-9]+-[0-9]+/ }

println '__目次__'
println()
hTags.each {
    def hTag = it.parent()
    def title = hTag.text().trim()
    int level = getHLevel(hTag)
    def indent = ' ' * level
    
    println "${indent}- <a href=\"${it.@href}\">${title}</a>"
}

int getHLevel(htag) {
    htag.name().substring(1).toInteger() - 1
}
```

##使い方
1. 上記コード中の `url` 変数に、目次を作りたい記事の URL を書く。
1. [NekoHTML](http://nekohtml.sourceforge.net/) から zip をダウンロードして、中に入っている `nekohtml.jar` と `xercesImpl-2.10.0.jar` を取得する。
1. 取得した jar をクラスパスに追加して上記コードを Groovy で実行。

```text:生成される目次
__目次__

- <a href="#1-1">特徴とか</a>
- <a href="#1-2">環境</a>
 - <a href="#2-1">Java</a>
 - <a href="#2-2">Google Guice</a>
- <a href="#1-3">使い方（基本）</a>
- <a href="#1-4">インターフェースの実装を指定する</a>
 - <a href="#2-3">@ImplementedBy アノテーションで実装クラスを指定する</a>
- <a href="#1-5">インジェクションできる場所について</a>
 - <a href="#2-4">フィールドインジェクション</a>
 - <a href="#2-5">メソッドインジェクション</a>
 - <a href="#2-6">コンストラクタインジェクション</a>
- <a href="#1-6">アノテーションでインジェクションするクラスを指定する</a>
- <a href="#1-7">インジェクションするインスタンスを指定する</a>
- <a href="#1-8">Provides メソッドでインジェクションするインスタンスを定義する</a>
- <a href="#1-9">Provider クラスでインジェクションするインスタンスを定義する</a>
 - <a href="#2-7">@ProvidedByアノテーションで Provider クラスを指定する</a>
- <a href="#1-10">Provider クラス自体をインジェクションする</a>
- <a href="#1-11">Provider クラスの get() メソッドでチェック例外をスローする</a>
- <a href="#1-12">インジェクションするインスタンスを生成するときのコンストラクタを指定する</a>
- <a href="#1-13">シングルトン</a>
 - <a href="#2-8">動作確認</a>
- <a href="#1-14">依存関係が解決できないときにエラーを無視する</a>
- <a href="#1-15">オンデマンドでインジェクションする</a>
- <a href="#1-16">static フィールドにインジェクションする</a>
- <a href="#1-17">インターセプター</a>
 - <a href="#2-9">特定のパッケージ直下にあるクラスのみ対象</a>
 - <a href="#2-10">特定のパッケージ以下にあるクラスのみ対象（サブパッケージも含む）</a>
 - <a href="#2-11">特定のパッケージ以外にあるクラスのみ対象</a>
 - <a href="#2-12">特定のアノテーションが付与されたクラス（メソッド）のみ対象</a>
 - <a href="#2-13">特定のクラス、およびそのサブクラスのみ対象</a>
 - <a href="#2-14">Matcher を自作する</a>
- <a href="#1-18">参考</a>
```

a タグ使えるの知らんかった。

##参考
- [Groovyでスクレイピングしてみる(NekoHTML編) - No Programming, No Life](http://npnl.hatenablog.jp/entry/20090428/1240929966)