mael を VBA に移植する試み
mael とは
mael とは、Markdown ファイルから、Excel ファイルを生成できる Python パッケージです。
参考文献
テストケースを Excel にしたいなら、以下の方が良いかもしれません。
- k-watanb/md_test_case_to_excel: Markdownで書かれたテスト仕様書をExcel形式に変換します。
- Markdown で書いたテスト仕様書を Excel に変換したい - 紙一重の積み重ね
なぜ VBA に移植しようと考えたか
いまどき VBA かよ、といのはごもっともですが…。
- 職場のセキュリティポリシー上、Python やモジュールをインストールできないため。
- VBA であれば、配布先に追加のプログラムを配布する必要がない。
どこまで再現するか
- yaml の処理はハードルが上がりそう -> config は Excel ブックにする?
- とりあえず config 周りは後回し
- 列幅は Excel での単位と一緒にしたい
- 列の型は、
String
とList
の二種類があるが、両方に対応するのか?- まずは
String
のみに対応する。
- まずは
-
mael init
に当たる操作を実現するか? -> とりあえず実装しない - まずは markdown から Excel シートに転記する部分を再現したい
- mael では複数のファイルを変換できるが、とりあえず単一ファイルの変換を実現する。
技術的課題
VBA で正規表現を扱うには?
参照設定で Microsoft VBScript Regular Expressions 5.5
を追加し、RegExp
オブジェクトを使用する。
参考文献
VBA で BOM なし UTF-8 なテキストファイルを読み込むには?
参照設定で Microsoft ActiveX Data Objects X.X Library
を追加し、ADODB.Stream
オブジェクトを使用する。
参考文献
- 【VBA】UTF-8のファイルを1行ずつ読み取る。 #ExcelVBA - Qiita
- BOM有無UTF-8テキストファイルの読み書き - つらつら Excel VBA
- 【Excel】テキストファイルの読み書き_ADODB.Stream編
VBA で動的リストや辞書を扱うには?
組み込みの Collection
や Dictionary
を使うか、.NET の System.Collections.ArrayList
を使う。
参考文献
- Excel VBAで使える可変長なコレクションまとめ #ExcelVBA - Qiita
- ArrayList クラス (System.Collections) | Microsoft Learn
- [VBA]ArrayListの使い方、ソート、動的配列
VBA でクラスを利用するには?
クラスモジュール
を利用する。
参考文献
- 簡単で便利なクラスを作って学ぶVBAクラスモジュール入門 - ExcelVBA - 和風スパゲティのレシピ
- VBAでクラスをつくる #オブジェクト指向 - Qiita
- ExcelVBAにおけるクラスモジュールの基本|藍鼠/ainez
- 【ExcelVBA】コンストラクタ(Initialize)に引数を渡せない問題 - 和風スパゲティのレシピ
- VBAのクラスでNewと同時に引数付きコンストラクタを起動する代替案 - えくせるちゅんちゅん
- VBA/VBScriptのクラスのコンストラクタに引数を渡したい #VBA - Qiita
- Excel VBAでコンストラクタに引数を渡す方法 | 縁紡ぐ
- 【vba】引数付きコンストラクタの一案 - わーぷろおじさん
VBA で列挙型を利用するには?
Enum
ステートメントを使う。
参考文献
Doxygen でのドキュメント化を考慮する
VB.NET 用のフィルタを流用する。いくつか種類があるが、awk 版 doxygen-vb-filter の使用を考慮する。
コメントは、VB.NET Style
を採用する。
参考文献
- Documenting your sources · sevoku/doxygen-vb-filter Wiki
- DoxygenにVB - vbfilter(awk): だらろぐ
- Excel VBAからDoxygenを用いてドキュメントを出力する #VBScript - Qiita
- Excel VBAからDoxygenを用いてドキュメントを出力する (2025 年版) #doxygen - Qiita
移植作業
変換のメイン処理部分を確認
mael の mael/main.py を確認し、引数に build
が指定された場合の処理 を確認する。
すると以下の記述で import された mael/excel_builder.py の convert が呼び出されていることが分かる。
from .excel_builder import convert
Excel への転記は composer のお仕事らしい
mael/excel_builder.py
の convert
の最終行 では、mael/composer.py で定義されている ExcelComposer クラスの compose メソッドが呼び出されている。
セルの内容を管理するのは StepItem クラス
mael/excel_builder.py
の StepItem クラス がセルの内容を管理するクラスである。
このクラスには、4つのプロパティと2つのメソッドがある。
プロパティ | 役割 |
---|---|
title | 列のタイトル |
type | 列のタイプ |
content_lines | 内容の行リスト |
content_items | 内容の項目リスト |
メソッド | 役割 |
---|---|
add_content_line | typeに応じて content_lines か add_content_items に内容を加える |
get_content | typeに応じて content_lines を結合した文字列か add_content_items を返す |
てか、Python はメソッドの返却型を複数指定できるのか…
若干、実装が異なるが、VBA で書くとこんな感じかな…
''' <summary>Class StepItem</sammary>
Option Explicit
Public title As String
Private itemType As ValueType
Private contentLines As Collection
Private contentItems As Collection
''' <summary>Constructor</sammary>
Private Sub Class_Initialize()
title = ""
itemType = TYPE_STRING
Set contentLines = New Collection
Set contentItems = New Collection
End Sub
Public Sub Init(title_ As String, type_ As ValueType)
title = title_
itemType = type_
End Sub
''' <summary>Add line of Content</sammary>
''' <params id="content"></param>
Public Sub AddContentLine(content As String)
If itemType = TYPE_STRING Then
contentLines.Add (content)
ElseIf itemType = TYPE_LIST Then
contentItems.Add (content)
End If
End Sub
''' <summary>Get lines of Content</sammary>
''' <returns>lines of Content</returns>
Public Function GetContent() As Collection
If itemType = TYPE_STRING Then
Set GetContent = contentLines
ElseIf itemType = TYPE_LIST Then
Set GetContent = contentItems
End If
End Function
まずは markdown から1行ずつ読み出しセルに転記してみる
'''<summary>Convert Markdown to Sheet</summary>
'''<params id="filePath"></params>
Sub Convert(filePath As String)
Dim adoStream As New ADODB.Stream
Dim rowNumber As Long
Dim line As String
With adoStream
.Type = adTypeText
.Charset = "UTF-8"
.LineSeparator = adLF
.Open
.LoadFromFile filePath
rowNumber = 1
Do Until .EOS
line = .ReadText(-2)
Cells(rowNumber, 1).Value = line
rowNumber = rowNumber + 1
Loop
.Close
End With
End SubSub Convert(filePath As String)
Dim adoStream As New ADODB.Stream
Dim rowNumber As Long
Dim line As String
With adoStream
.Open
.Type = adTypeText
.Charset = "UTF-8"
.LineSeparator = adLF
.LoadFromFile filePath
rowNumber = 1
Do Until .EOS
line = .ReadText(-2)
Cells(rowNumber, 1).Value = line
rowNumber = rowNumber + 1
Loop
.Close
End With
End Sub
テスト仕様書をGitで管理する: MarkdownからExcelを作る #テストシナリオの例にある markdown ファイルを読み込ませると下図のようになる。
文書のタイトルを取得してシートを追加する部分を作成してみる
mael/excel_builder.py
の 167行目から始まる処理を移植する。
まず、シート名は使えない文字があのでそれを正規化する関数を用意する。使えない文字を検索したら、Google AI がコード例を出してくれたので流用する。
''' <summary>Normalize Sheet Name</sammary>
Function EscapeSheetName(ByRef sheetName As String) As String
Dim reservedChars As String
Dim i As Long
Dim escapedName As String
reservedChars = "\/?*" & Chr(34) & "<>" & "|"
escapedName = sheetName
For i = 1 To Len(reservedChars)
escapedName = Replace(escapedName, Mid(reservedChars, i, 1), "_")
Next i
EscapeSheetName = escapedName
End Function
よく見たら、テストコードまで含まれていた。
Sub TestEscapeSheetName()
Dim sheetName As String
sheetName = "テストシート\/?*" & Chr(34) & "<>|"
Debug.Print EscapeSheetName(sheetName) ' Output: テストシート_____"____
End Sub
前項のコードで Do Until .EOS
ブロックの前に以下のコードを追加した。
' set name
Do Until .EOS
line = .ReadText(-2)
With New RegExp
.Pattern = "^#[^#]\s*(\S.*)\s*$"
Set mc = .Execute(line)
If mc.Count > 0 Then
ActiveWorkbook.Sheets.Add
ActiveSheet.Name = EscapeSheetName(mc(0).SubMatches(0))
Exit Do
End If
End With
Loop
If .EOS Then
MsgBox "Can not find title."
Exit Sub
End If
Summary の処理
mael/excel_builder.py
の 178行目~202行目までの処理を移植する。
' set summary
Dim hasSammary As Boolean
hasSammary = False
Do Until .EOS
line = .ReadText(-2)
With New RegExp
.Pattern = "^##\s*Summary\s*$"
If .Test(line) Then
hasSammary = True
Exit Do
End If
End With
Loop
If hasSammary Then
With Cells(rowNumber, 1)
.Value = "Summary"
.Font.Bold = True
rowNumber = rowNumber + 2
End With
End If
' read summary lines
Do Until .EOS
line = .ReadText(-2)
With New RegExp
.Pattern = "^##\s*(List|Steps|Rows)\s*$"
If .Test(line) Then
rowNumber = rowNumber + 1
Exit Do
End If
End With
Cells(rowNumber, 1).Value = RTrim(line)
rowNumber = rowNumber + 1
Loop
表の内容にあたる部分の処理
続いて、表の内容にあたる部分を steps
という Collection
型の変数に積んでいく。
' read steps
Dim steps As New Collection
Dim stepDict As New Scripting.Dictionary
Dim columns As New Scripting.Dictionary
Dim item As StepItem
Dim title As String
Set item = Nothing
Do While True
line = .ReadText(-2)
If .EOS Then
If Not item Is Nothing Then
stepDict.Add item.title, item.GetContent()
End If
If stepDict.Count > 0 Then
steps.Add stepDict
End If
Exit Do
End If
With New RegExp
.Pattern = "^\s*---\s*$"
If .Test(line) Then
If Not item Is Nothing Then
stepDict.Add item.title, item.GetContent()
Set item = Nothing
End If
If stepDict.Count > 0 Then
steps.Add stepDict
Set stepDict = New Scripting.Dictionary
End If
GoTo CONTINUE
End If
.Pattern = "^#{3,}\s*(\S.*\S|\S)\s*$"
Set mc = .Execute(line)
If mc.Count > 0 Then
If Not item Is Nothing Then
stepDict.Add item.title, item.GetContent()
End If
title = mc(0).SubMatches(0)
If Not columns.Exists(title) Then
columns.Add title, ""
End If
Set item = New StepItem
item.Init title, TYPE_STRING
GoTo CONTINUE
End If
If Not item Is Nothing Then
item.AddContentLine (RTrim(line))
End If
End With
'Cells(rowNumber, 1).Value = line
'rowNumber = rowNumber + 1]
CONTINUE:
Loop
そして、実際の表に転記する処理を実装。
Dim key As Variant
Dim colNumber As Long
colNumber = 1
For Each key In columns.Keys
titleRow = rowNumber
With Cells(rowNumber, colNumber)
.Value = key
.Font.Bold = True
.HorizontalAlignment = xlCenter
End With
colNumber = colNumber + 1
Next
rowNumber = rowNumber + 1
Dim obj As Object
Dim content As Collection
For Each stepDict In steps
colNumber = 1
For Each key In columns.Keys
If stepDict.Exists(key) Then
Set content = stepDict.item(key)
Cells(rowNumber, colNumber).Value = JoinCollection(content)
End If
colNumber = colNumber + 1
Next
rowNumber = rowNumber + 1
Next
最後に罫線を引く。
' format borders
With Range(Cells(titleRow, 1), Cells(rowNumber - 1, columns.Count))
For index = xlEdgeLeft To xlInsideHorizontal
With .Borders(index)
.LineStyle = xlContinuous
.ColorIndex = 0
.TintAndShade = 0
.Weight = xlThin
End With
Next
End With
Convert 呼び出し部分の実装
ユーザーに変換したい markdow ファイルを選択させ、Convert
プロシージャを呼ぶ。
'''<summary>Control Build Process</summary>
Sub Build()
Dim filePath As Variant
filePath = Application.GetOpenFilename("markdown,*.md")
If filePath = False Then
Exit Sub
End If
Convert CStr(filePath)
End Sub
リボンをつける
リボンを編集しようと思い、Office Custom UI Editor をダウンロードしようと思ったら、いつの間にか、Public archive
にされてた… 😅
Office RibbonX Editor が、Office Custom UI Editor
から fork したらしいのでそちらを利用することにする。
Microsoft の Copilot にアイコンを作成してもらった。
リボンから実行できるように Build
プロシージャに引数を追加する。
Sub Build(control As IRibbonControl)
そして、customUI.xml
を作成する。
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
<ribbon>
<tabs>
<tab id="maelTab" label="mael" insertAfterMso="TabView">
<group id="maelGroup" label="maelForVBA">
<button id="BuildButton1" label="Build" size="large" onAction="Build" image="Build" />
</group>
</tab>
</tabs>
</ribbon>
</customUI>
できたリボンは、こんな感じ。
現状の変換結果
テスト仕様書をGitで管理する: MarkdownからExcelを作る #テストシナリオの例にある markdown ファイルを読み込ませると下図のようになる。
近いうちに column_config に対応して、本来の出力に近づけたい。
GitHub のリポジトリ
yasumichi/maelForVBA: mael for VBA is a partial port of mael.
ここまでの成果をバージョン 0.1 として、プレリリースした。
追記
予期せぬ markdown ファイルが与えられた場合に異常終了するバグを修正し、0.1.1 をプレリリースした。
Discussion