Github の Wiki を VBA ソースから生成する

#経緯

VBAのライブラリを作成したので、マニュアルを書きたいが、クラスが多くて大変。自動的に作成したい。

- JavaDoc みたいにソースの看板に記述したものが GitHub の Wiki に自動的になったらいいな。
- GitHub の Wiki 向けに作成するのであれば MarkDown が良い。
- Wiki のサイドバー部分も一緒に生成し、リンクを貼りたい。

適当にググってみたがツールはなさそうなので作ってみよう。

#VBA の看板を考える

VBAの看板に @なにがし とか HTML/XML のタグを入れてもいいけど、MarkDown 自体簡略した書き方だし、HTMLやXMLからMarkDown を生成するのは論外だということで、直接 MarkDown を書くことにする。  
コメントに書くということと、MarkDown であることが判別する必要があるため、MarkDown の場合には、行頭に「'>」をつけることにする。
段落内の改行は行末尾に**半角スペース２個**だが、VBEが勝手に消してしまうので仕方なく```<BR>```を書くことにする。

## 看板イメージ

VBAソースにMarkDownを記述する。

```vb
'>---
'>#### FileExists(FileIO)
'>
'>**Syntax**
'>
'>```
'>flg = FileIO.FileExists(strFile)
'>```
'>
'>**Parameters**
'>
'>|Name|Required/Optional|Data type|Description|
'>---|---|---|---
'>|strFile|必須|String|存在チェックを行うファイルを指定|
'>
'>**Return Value**
'>
'>|Name|Data type|Description|
'>---|---|---
'>|flg|Boolean|存在する場合:True / 存在しない場合:False|
'>
'>
'>**Remarks**
'>
'>ファイル存在チェック<br>
'>指定されたファイルの存在チェックを行う
'>
'>**Example**
'>
'>* None
'>
'>**See also**
'>
'>* FileIO.FolderExists
'>
Public Function FileExists(ByVal strFile As String) As Boolean
 
    With FSO
        FileExists = .FileExists(strFile)
    End With

End Function
```

生成後のイメージは以下の通り。Microsoft のマニュアルページを参考にした。

---
<a name="FileExists"></a>
#### 2.3.15.1 FileExists(FileIO)

**Syntax**

```
flg = FileIO.FileExists(strFile)
```

**Parameters**

|Name|Required/Optional|Data type|Description|
---|---|---|---
|strFile|必須|String|存在チェックを行うファイルを指定|

**Return Value**

|Name|Data type|Description|
---|---|---
|flg|Boolean|存在する場合:True / 存在しない場合:False|


**Remarks**

ファイル存在チェック
指定されたファイルの存在チェックを行う

**Example**

* None

**See also**

* FileIO.FolderExists

---


まぁ、ソースとしての可読性はアレだけど Wiki になれば見栄えはばっちりなので妥協することにする。そもそも両方の見栄えというよりも

* ソースの看板はかんたんに書けること
* Wiki は読みやすいこと

を考えればこれでよいかと。

これを看板に書いておけば自動的にMarkDownファイルが生成されるようにする。
クラス名が「FileIO」なら「FileIO.md」がファイル名になる。

# 目次

目次の作成について考える。
GitHUb の Wiki に目次を生成するためには、 "_Sidebar.md" というファイルに MarkDown を書けば良い。

## 目次案

マニュアルのページが大きくなって、使用方法が見づらくなるので使用方法のページを先に表示する。

```
目次
1 使用方法
2 リファレンス
2.1 標準モジュール
2.2 インターフェイス
2.3 クラス
```

## 章番号

MarkDown で見出しを作成しても章番号が生成されないので、CSS か VBA で生成する必要がある。
GitHub の CSS って弄れるんか？よくわからん。MarkDown 生成の際に付加することにする。


# 生成ソース

MarkDown 生成ソースは以下の通り。VBAでVBAソースをなめるため、Excelのオプションで「VBA プロジェクト オブジェクト モデル へのアクセスを信頼する」をチェックする必要がある。
![vbadev.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69810/9fb9447c-7432-971b-6158-de6dacb478d2.png)


あちこち見慣れないクラスが登場すると思うが、作成したライブラリの関数なので気が向いたらみてみてほしい。

Hidennotare
https://github.com/RelaxTools/Hidennotare

ドキュメント生成モジュール(Hidennotareをgitやwikiで管理するためのモジュール)
https://github.com/RelaxTools/Hidennotare/blob/master/src/Document.bas

```vb
'-----------------------------------------------------------------------------------------------------
'>### Document 標準モジュール
'>
'>**Remarks**
'>
'>- ドキュメント生成モジュール(Hidennotareをgitやwikiで管理するためのモジュール)
'>
'-----------------------------------------------------------------------------------------------------
Option Private Module
Option Explicit

'Wiki URL
Private Const TARGET_URL As String = "https://github.com/RelaxTools/Hidennotare/wiki/"
'-----------------------------------------------------------------------------------------------------
' Markdown出力
' Markdownがある行の先頭に「'>」があるものについてファイルに出力する。
'-----------------------------------------------------------------------------------------------------
Sub OutputMarkDown()
    
    Dim obj As Object
    Dim strFolder As String
    Dim strFile As String
    Dim SB As IStringBuilder
    Dim strBuf As String
    Dim strMark As String
    Dim i As Long
    Dim TC As IList
    Dim FP As Integer
    Dim bytBuf() As Byte
    Dim IW As IWriter
    
    On Error GoTo e
    
    '目次作成用
    Set TC = New ArrayList
    
    '章番号を付加するレベル
    Const Level As Long = 4
    
    '目次を作成するレベル
    Const ContentsLevel As Long = 3
    
    Dim No1() As Long
    Dim No2() As Long
    Dim No3() As Long

    ReDim No1(1 To Level)
    ReDim No2(1 To Level)
    ReDim No3(1 To Level)

    '標準モジュールのスタート
     No1(1) = 2
     No1(2) = 1
     No1(3) = 0

    'インターフェイスのスタート
     No2(1) = 2
     No2(2) = 2
     No2(3) = 0

    'クラスのスタート
     No3(1) = 2
     No3(2) = 3
     No3(3) = 0
    
    'Hidennotare.wiki フォルダを作成
    strFolder = ThisWorkbook.Path & ".wiki"
    FileIO.CreateFolder strFolder
    
    
    'VBComponents の取得順が アルファベット順ではないので、SortedDictionary を使用。
    Dim dic As IDictionary
    Set dic = New SortedDictionary
    
    For Each obj In ThisWorkbook.VBProject.VBComponents
        dic.Add obj.Name, obj
    Next
    
    Dim Key As Variant
    For Each Key In dic.Keys
        
        Set obj = dic.Item(Key)
        
        'モジュール名.md を作成する。
        strFile = FileIO.BuildPath(strFolder, obj.Name & ".md")
        
        With obj.CodeModule
            
            Set SB = StringBuilder.CreateObject
            
            For i = 1 To .CountOfLines
                
                '指定位置から１行取得
                strBuf = .Lines(i, 1)
                
                If Left$(strBuf, 2) = "'>" Then
                    
                    '------------------------------------------
                    ' 章番号の生成
                    '------------------------------------------
                    strMark = Mid$(strBuf, 3)
                    Select Case True
                        
                        '標準モジュール
                        Case obj.Type = 1
                           SB.Append LevelNo(strMark, No1(), Level, TC, ContentsLevel, obj.Name)
                        
                        '1文字目が"I"、2文字目が大文字の場合、インターフェース
                        Case RegExp.Test(obj.Name, "^I[A-Z]") And obj.Name <> "ICON"
                           SB.Append LevelNo(strMark, No2(), Level, TC, ContentsLevel, obj.Name)
                        
                        'その他クラス
                        Case Else
                           SB.Append LevelNo(strMark, No3(), Level, TC, ContentsLevel, obj.Name)
                    
                    End Select
                    
                End If
            Next i
        
            '対象があれば出力する
            If SB.Length > 0 Then
                
                'ファイルを空にする。
                FileIO.TruncateFile strFile
                
                'UTF8 & LF で保存
                With TextWriter.CreateObject(strFile, _
                                             NewLineCodeConstants.NewLineCodeNone, _
                                             EncodeConstants.EncodeUTF8, _
                                             OpenModeConstants.OpenModeOutput, _
                                             False)
        
                    .WriteData SB.ToString(vbLf)
                
                End With
            
            End If
            
            Set SB = Nothing
        
        End With
    
    Next
    
    'Wikiの目次作成
    If TC.Count > 0 Then
    
        Dim strStatic As String
        
        'Wikiの目次ファイル名
        strFile = FileIO.BuildPath(strFolder, "_Sidebar.md")
        
        '------------------------------------------
        ' 目次の静的コンテンツ部分を取得
        '------------------------------------------
        strStatic = GetStaticContents(strFile)
        
        'ソート
        TC.Sort New ExplorerComparer
        
        '目次作成
        TC.Insert 0, "#### 2 リファレンス"
        TC.Insert 1, "##### 2.1 標準モジュール"
        For i = 0 To TC.Count - 1
            If Core.StartsWith(TC.Item(i), "[2.2") Then
                TC.Insert i, "##### 2.2 インターフェイス"
                Exit For
            End If
        Next
        For i = 0 To TC.Count - 1
            If Core.StartsWith(TC.Item(i), "[2.3") Then
                TC.Insert i, "##### 2.3 クラス"
                Exit For
            End If
        Next
        
        '元ファイルをクリア
        FileIO.TruncateFile strFile
        
        '静的コンテンツと生成した目次をUTF8 & LF にて出力。
        With TextWriter.CreateObject(strFile, _
                                     NewLineCodeConstants.NewLineCodeNone, _
                                     EncodeConstants.EncodeUTF8, _
                                     OpenModeConstants.OpenModeOutput, _
                                     False)
        
            .WriteData strStatic
            .WriteData Join(TC.ToArray(), vbLf)
        
        End With
    End If
    
    MsgBox "生成しました！", vbInformation, "Markdown"
    
    Exit Sub
e:
    If Err.Number = 70 Then
        If Message.Question("他のプログラムで開いています。再試行しますか？") Then
            Message.Critical "処理を中断しました。"
        Else
            Resume
        End If
    End If
    Message.Critical "予期しないエラーが発生しました。{0},{1}", Err.Number, Err.Description
End Sub
'---------------------------------------------------
' 章番号生成
'---------------------------------------------------
Private Function LevelNo(ByVal strBuf As String, _
                         No() As Long, _
                         ByVal lngLevel As Long, _
                         TC As IList, _
                         ByVal lngContentsLevel As Long, _
                         ByVal strName As String) As String

    Dim col As Collection
    Dim SB As IStringBuilder
    Dim lngLen As Long
    Dim i As Long
    Dim strID As String
    
    '章番号(###～)の場合
    Set col = RegExp.Execute(strBuf, "^#+ ")
    If col.Count > 0 Then
    
        lngLen = Len(col(1).Value) - 1
        
        Dim strLeft As String
        Dim strRight As String
        
        strLeft = col(1).Value
        strRight = Mid$(strBuf, col(1).Length)
        
        '章番号生成レベル以上であれば、章番号作成
        If lngLen <= lngLevel Then

            '章番号をカウントアップ
            No(lngLen) = No(lngLen) + 1

            '現レベル以下の番号をクリア
            For i = lngLen + 1 To lngLevel
                No(i) = 0
            Next

            '章番号の生成
            Set SB = StringBuilder.CreateObject
            For i = 1 To lngLen
                SB.Append CStr(No(i))
            Next
            
            LevelNo = SB.ToString(".", strLeft, strRight)
        
            '目次作成レベル以上であれば目次作成
            If lngLen <= lngContentsLevel Then
            
                Dim strContent As String
                
                'Markdown のリンク見出しとリンク作成
                strContent = "[" & Mid$(LevelNo, InStr(LevelNo, " ") + 1) & "](" & _
                             TARGET_URL & Replace$(strName, " ", "-") & ")  "
                
                '目次のエリアが限られるので種類は削除
                strContent = Replace$(strContent, " クラス", "")
                strContent = Replace$(strContent, " インターフェイス", "")
                strContent = Replace$(strContent, " 標準モジュール", "")
            
                TC.Add strContent
            
            End If
        
        Else
            LevelNo = strBuf
        End If
        
        'クラス及びメソッドのアンカー作成
        Dim lngPos As Long
        lngPos = InStr(strRight, "(")

        If lngPos > 0 Then
            strID = Trim$(Mid$(strRight, 1, lngPos - 1))
        Else
            strID = Trim$(strRight)
        End If

        LevelNo = "<a name=""" & strID & """></a>" & vbLf & LevelNo
        
        Exit Function
    End If

    'See also のリンクを生成
    If RegExp.Test(strBuf, "^\* [A-Za-z0-9.]+") Then
    
        strID = Mid$(strBuf, 3)
        If UCase(strID) <> "NONE" Then
            LevelNo = "* [" & strID & "](" & TARGET_URL & Replace$(strID, ".", "#") & ")"
        Else
            LevelNo = strBuf
        End If
        Exit Function
    End If

    LevelNo = strBuf

End Function
'---------------------------------------------------
'目次から静的コンテンツ部分を抜き出す
'---------------------------------------------------
Private Function GetStaticContents(ByVal strFile As String) As String

    Dim SB As IStringBuilder
    Dim IC As ICursor
    
    GetStaticContents = ""
    
    Set SB = StringBuilder.CreateObject
    
    Set IC = TextReader.CreateObject(strFile, _
                                     NewLineCodeConstants.NewLineCodeLF, _
                                     EncodeConstants.EncodeUTF8)

    Do Until IC.Eof
    
        If Core.StartsWith(IC, "#### 2") Then
            Exit Do
        End If
        
        SB.Append IC
        
        IC.MoveNext
    Loop

    If SB.Length > 0 Then
        GetStaticContents = SB.ToString(vbLf, "", vbLf)
    End If
    
End Function
```

# 生成した Wiki

以下、Github Wiki に生成したマニュアルをUpしてある。
https://github.com/RelaxTools/Hidennotare/wiki

結構見やすくできたと思う。まだ間違いが沢山あると思うがとりあえず、これでメンテナンスが楽になる。