背景
定期的にプログラミング研修の講師の仕事をしています。その研修で使用する教材はほとんどがパワポで作成されています。受講生に対しては、パワポから出力したPDFを研修のテキストとして配布し、パワポのスライドをプロジェクターに映しながら講義をします。その研修はもう何年も前からこの運用方法で実施していますが、最近はパワポで教材を作ることに色々と不便に感じる部分があり、マークダウンに移行することにしました。この記事はその過程と移行する際に使用したツールや技術に関するまとめ、及び備忘録です。
パワポで教材を作ることの問題点
パワポでプログラミングの教材を作ることには以下のような問題点がありました。
- 共同制作が難しい
- 編集履歴の管理ができない
- Macユーザーに不利
- アクセシビリティが悪い
- ソースコードのコピペが不便
共同制作が難しい
講師は私一人ではなく、複数名存在します。そのため、講師間で共同編集できると効率が良いですが、パワポのファイルで教材を管理していると共同制作は難しくなります。OneDriveやSharePointを使うことで共同編集できなくもないですが、ファイル単位での管理しかできないのが欠点です。
編集履歴の管理ができない
パワポで資料を作ると、修正の履歴が管理できないのもつらいです。
ファイルを残しておいて履歴管理する方法もありますが、テキストファイルではないので差分だけを簡単に確認することが難しく、変更部分だけを確認するのが不便です。
Macユーザーに不利
仕事用のPCでMacを使う方もいますが、Macの場合そもそもパワポがインストールされていないことも珍しくありません。そうなるとそもそも編集することが難しい。Keynoteを使ったりOffice365を新規にインストールすればある程度解決できますが、それでもOSの違いで若干のレイアウト崩れは発生することがあり、全体としてMacユーザーが不便することになります。
アクセシビリティが悪い
テキストがPDFになっているので、講義をする際に毎回異なるファイルを開いて確認する必要があります。過去の復習をする場合にもわざわざファイルを探して開く必要があり、必要な情報のアクセシビリティが悪いです。研修事業を実施している企業の中には、テキストを紙で配布する企業も少なくありません。PDFの場合は電子データである分紙に比べれば多少ましかもしれないですが。それでもファイルを一つ一つ開かなければ内容を確認できないのは不便です。
ソースコードのコピペが不便
パワポで教材を作る一番のデメリットはおそらくこれです。プログラミングの学習をする場合、教材からソースコードをコピペして実行しながら確かめることは少なくありません。ですが、パワポの場合1スライドに載せられる情報量は限られているので、ソースコードの一部を省略しなければいけなかったり、ページを跨いでコードを記述する必要が出てきます。まだ、謎に変な空白文字が入っていて、プログラムの実行に影響が出たりすることもあります。これは学習する側の立場からすると非常に不便です。
マークダウンによる解決
これらの問題を解決するため、パワポの教材を廃止し、全てマークダウン形式のファイルにしてGitで管理することにしました。教材をマークダウンにすれば中身は基本的にテキストデータになるので、差分の確認が容易です。OSの違いによる問題も発生しません。作業者がGitの扱いに慣れていれば共同作業も差分の確認も簡単です。
また、マークダウンからHTMLへ変換し、Webサイトとして公開することで、アクセシビリティの向上が見込め、ソースコードをコピペする作業も容易になります。つまり、先に述べたパワポでの問題点をほぼ解決できます。
完成系のイメージ
マークダウンに移行した後、最終的にどういう形で教材を見えるかするのかを色々考えました。
一つ一つの記事をQiitaにアップしてURLを共有するという手も考えましたが、記事の数が増えると管理が面倒になりそう、かつ共同編集ができない(Qiita Teamならできるらしいけど)という点からQiitaは断念しました。
結論として、マークダウンを自分でどうにかHTMLに変換し、それぞれのページに簡単にアクセスできるトップページを作成し、GitHub Pages等にデプロイする形をとることにしました。サイトの見た目としては以下のような感じです。教材にはそれぞれカテゴリがあります(Web, Java, PHP, SQLのようなもの)。そのカテゴリ毎に細かい単元のWebページへのリンクが一覧に並ぶイメージです。各単元のWebページでは、Qiitaの記事のような見栄えで表示されることを目指しました。
階層構造のイメージは以下です。
|-DocumentRoot
|-index.html
|-*.md : 教材の内容
|-html : mdから変換したhtmlを格納
|-pdf : slidevでビルドしたPDFを格納
|-images : 画像ファイルの格納
|-script : 変換系のスクリプトを格納
|-css : htmlが参照するcssを格納
|-js : htmlが参照するjsを格納
マークダウンも別途フォルダを分けた方が良いとは思いましたが、後述するslidevというツールの都合でドキュメントルート配下に置くことにしています。
パワポからマークダウンへの変換
パワポの教材のファイル数は少なく見積もっても70以上ありました。一つ一つ手動でマークダウンへ変換していくのは非常に手間だと思い、どうにか自動化できないか色々考えましたが、結局手動で実施することにしました。
考えた案としては
・PowerShellを使ってテキストデータを取得してファイルに書き込む
・パワポをzipに変換して、中身のxmlファイルからデータを取り出す
・マクロを作成して中身のデータをテキストデータとして出力する
などなど。どれもやろうと思えば不可能ではなさそうだけど、分析調査 + コーディングのコストと手動で作成するコストを天秤にかけた結果、手動の方が速くて簡単という結論になりました。スクリプトを作ったとしても使う場面は一度きりだし、今後同じようなシチュエーションに遭遇するとも考えにくかったので。。
マークダウンの作成はVS Codeで実施しました。当初は画像ファイルの移行が面倒そう、と思いましたが、拡張機能で画像をサクッとコピペできる便利ツールがあったので、そこまで大変ではありませんでした。
意外と大変だったのがテーブル情報の移行。
パワポで作られたテーブルですが、中身のテキストだけをまとめてコピペすることができず、要素を1つずつしかコピペできなかったため、マークダウンのテーブルを作るのが結構大変でした。パワポのテーブルには要注意です。
マークダウンからスライドの作成
そもそもなぜ元の教材がパワポで作っていたのかというと、研修ではプロジェクターを使ってスクリーンに教材を映しながら講義を進めていく形を取っているからでしょう。おそらくその研修事業はマークダウンが今ほど一般的ではない時から実施しているので、パワポのようなプレゼンソフトを使って教材を作るのはある意味必然だったとも言えるでしょう。
今回、先に述べた問題を解決するためにパワポからマークダウンに移行することにしましたが、複数名を相手に一斉に教える講義スタイルである以上、やはりスライド資料も欲しいところです。先にも少し触れましたが、何か良い方法がないか探していたところ、マークダウンからスライドを作成できるツールがありました。
他にもいくつかライブラリはあるようでしたが、このライブラリが使いやすそうだったのでこちらを採用しました。node.jsのライブラリで、作成したマークダウンを指定してビルドすることでスライド資料として使えるSPAが作成されます。デプロイすることでWeb上から参照可能で、オプションの指定によりPDFファイルとして出力することも可能です。
細かい使い方に関してはQiitaでもいくつか記事が上がっているので、そちらを参照ください。私も参考にさせていただきました。ありがとうございます。
ビルドとPDFのコピーを自動化
先に述べた通り教材用のファイルは70以上あるため、一つ一つをSPAとしてビルドしてサーバー環境にデプロイするのはさすがにリソースの無駄遣いです。slidevでは拡張機能によりPDFへの出力ができるので、スライドから出力したPDFをまとめて一つのWebサイトにアップし、PDFをいつでもダウンロードできる仕組みにします。
ただ、全てのマークダウンの修正が入るたびに毎回手動デプロイしてPDFを所定のフォルダにコピーするのも面倒です。ビルド&PDFのコピーする処理を自動化しました。自動化のスクリプトは何で書いても良かったですが、Slidevがnode.jsのライブラリというなんとなくの理由からnodeで作成しました。スクリプトは以下の内容を載せます。
const fs = require('fs')
const path = require("path");
const { execSync } = require('child_process')
// 対象外の一覧
const exclusions = ['README.md', "index.md", "DB.md", "Java.md", "PHP.md", "Web.md",
"アルゴリズム.md", "インフラ.md", "開発工程.md", "情報処理.md"]
// マークダウンファイルの一覧取得
const markdowns = fs.readdirSync('./').filter(e => path.extname(e) === '.md');
// PDFファイルの一覧取得
const pdfs = fs.readdirSync('PDF/').filter(e => path.extname(e) === '.pdf');
// PDF作成更新対象のファイル取得
// PDFがない または マークダウンが更新されたファイルを対象に
const target = markdowns.filter(m => {
if(exclusions.find(e => e === m)) return false
const found = pdfs.find(p => path.basename(m, '.md') === path.basename(p, '.pdf'))
if (found === undefined) {
return true
}
if (fs.statSync(m).mtime > fs.statSync('pdf/' + path.basename(m, '.md') + '.pdf').mtime) {
return true
}
return false
})
console.log(target);
// ビルド&PDFファイルのコピー
target.forEach(e => {
try {
execSync('npm run build -- ' + e)
} catch(error) {
console.error(error)
}
fs.copyFile('dist/slidev-exported.pdf', 'PDF/' + path.basename(e, '.md') + '.pdf', (err) => {
if (err) {
console.log(err.stack);
}
else {
console.log('Done.');
}
});
})
マークダウンからHTMLの作成
そもそもマークダウンで教材を作りたかったのは、教材をWebページとして公開し、アクセシビリティを上げたかったという目的もあります。パワポからマークダウンへ変換しても、マークダウンのまま受講生に提供しては意味がありません。そこでマークダウンからHTMLを作成する方法を色々探しながら試しました。
結論としては、ライブラリを使って変換プログラムを自作することにしました。
VS Codeの拡張機能で変換できるものや、マークダウンをコピペで張り付けるとHTMLに変換してくれるWebサービスなどもありましたが、柔軟性やスタイルの適用などを諸々考えると変換プログラムを自作した方が後々のためになりそうだったので。
ライブラリは、Qiitaがqiita_markderというライブラリを公開してくれていたので、これを使うことにしました。
RubyのライブラリなのでRubyで書く必要があります。Rubyでの開発はしたことがなかったので環境構築を含めて試行錯誤しながら作ることになりましたが、意外と何とかなりつつ勉強にもなったので良かったです。
以下は環境構築とプログラムの備忘録です。
1.Ruby(とgem)を適当にインストール
2.qiita_markerのインストール
gem install qiita_marker
3.rubyで変換プログラムの作成
require "qiita_marker"
def output_html(item)
title=File.basename(item, ".md")
body=File.read(item)
# slidev用の先頭部分削除
body.slice!(0, body.index("#") - 1)
#画像ファイルのパスを相対パスに変更
body=body.gsub(/\/images/, '../images')
# HTMLへの変換オプション
body=QiitaMarker.render_html(body, [:SOURCEPOS, :HARDBREAKS, :UNSAFE, :TABLE_PREFER_STYLE_ATTRIBUTES], [:table, :tasklist, :strikethrough, :autolink])
# テンプレートの定義
text=<<EOS
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>#{title}</title>
<link rel="stylesheet" href="../css/prism.css" type="text/css">
<link rel="stylesheet" href="../css/styles.css" type="text/css">
</head>
<body>
<div id="content">
#{body}
</div>
<script src="../js/prism.js"></script>
</body>
</html>
EOS
return text
end
# メイン処理
Dir.foreach('../.') do |item|
if File.extname(item) == '.md' then
# 引数でファイル名指定された場合はそれだけ対象にする
if ARGV[0] != nil && ARGV[0] != item then
next
end
text = output_html('../' + item)
# ファイルの書き込み
f = File.open('../html/' + File.basename(item, ".md") + ".html", "w")
f.puts(text)
end
end
Rubyのプログラムはそこまで難しいことはしておらず、基本的にディレクトリの中身でループしながらマークダウンのファイルを読み込んでHTMLファイルを出力しているだけです。さすがにレイアウトまでいい感じに出力してくれるわけではないので、CSSは自作してそれを読み込むようにしました。引数を指定することでファイル単位での出力も可能としました。
Webサイトのトップページの作成
これまでの過程でマークダウンからスライドのPDFや単体のWebページを作成することには成功しました。
あとはリンクからそれぞれのページやPDFにアクセスするためのトップページを作成する必要があります。
実はこの部分が一番苦戦しました。。
自分で作成すれば簡単といえば簡単ですが、新しくマークダウンを増やしたときにトップページの内容を動的に作成する仕組みが欲しかったので、そ実現方法を考えることに時間を使いました。
結論としては以下の方法でindex.htmlを作成することにしました。
- カテゴリとサイト名を定義した設定ファイルを用意
- 設定ファイルをもとに、カテゴリ単位のリンク一覧のマークダウンを作成するスクリプトを用意・実行
- カテゴリ単位のマークダウンからindex.html作成用のマークダウンを作成
ルールとして、mdファイルのファイル名は、先頭がカテゴリ名から入るという前提になります。
設定ファイルとスクリプトは以下のような内容です。
{
"title": "IT技術者養成講座",
"category": [
"Web", "情報処理", "アルゴリズム", "インフラ", "DB", "Java", "PHP", "開発工程"
]
}
設定ファイルからカテゴリ単位のリンクの一覧を作成するスクリプト。
const path = require("path");
const fs = require('fs');
const json = JSON.parse(fs.readFileSync('./index.json', 'utf8'));
json.category.forEach(category => {
const glob = require("glob");
let searchFileName = "../" + category + "\_*.md";
const files = glob.sync(searchFileName);
let body = `## ${category}\n\n`;
files.forEach(file => {
let fileName = path.basename(file, ".md");
let dispName = fileName.replace(category + '_','');
body += ` [${dispName}](./html/${fileName}.html)\n`;
body += ` [${dispName}(PDF)](./PDF/${fileName}.pdf)\n\n`;
});
fs.writeFile('../' + category + '.md', body, (err) => {
// 書き出しに失敗した場合
if(err){
console.log("エラーが発生しました。" + err)
throw err
}
// 書き出しに成功した場合
else{
console.log(category + '.md ' + "ファイルが正常に書き出しされました")
}
});
});
出力結果は以下のようになります。
## Web
[01_環境構築](./html/Web_01_環境構築.html)
[01_環境構築(PDF)](./PDF/Web_01_環境構築.pdf)
[02_HTML](./html/Web_02_HTML.html)
[02_HTML(PDF)](./PDF/Web_02_HTML.pdf)
[03_CSS](./html/Web_03_CSS.html)
[03_CSS(PDF)](./PDF/Web_03_CSS.pdf)
[04_Git](./html/Web_04_Git.html)
[04_Git(PDF)](./PDF/Web_04_Git.pdf)
カテゴリごとのリンクの一覧からindex.mdを作るスクリプト
const fs = require('fs');
const json = JSON.parse(fs.readFileSync('./index.json', 'utf8'));
let body = `# ${json.title}\n\n`;
json.category.forEach(category => {
body += `## [${category}](./html/${category}.html)\n\n`;
});
body += "---\n\n";
json.category.forEach(category => {
let text = fs.readFileSync(`../${category}.md`, 'utf8');
body += text.toString();
});
fs.writeFile('../index.md', body, (err) => {
// 書き出しに失敗した場合
if(err){
console.log("エラーが発生しました。" + err)
throw err
}
// 書き出しに成功した場合
else{
console.log('index.md ' + "ファイルが正常に書き出しされました")
}
});
index.mdからindex.htmlを作るスクリプト。
最初のマークダウンからhtmlに変換するスクリプトとほぼ同じですが、適用するCSSなどを別にしたかったこともあり、別で作ることにしました。
require "qiita_marker"
item = "../index.md"
title=File.basename(item, ".md")
body=File.read(item)
#画像ファイルのパスを相対パスに変更
body=body.gsub(/\/images/, './images')
# HTMLへの変換オプション
body=QiitaMarker.render_html(body, [:SOURCEPOS, :HARDBREAKS, :UNSAFE, :TABLE_PREFER_STYLE_ATTRIBUTES], [:table, :tasklist, :strikethrough, :autolink])
# テンプレートの定義
text=<<EOS
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./css/styles.css" type="text/css">
<title>#{title}</title>
</head>
<body>
<div id="content">
#{body}
</div>
</body>
</html>
EOS
# ファイルの書き込み
f = File.open('../index.html', "w")
f.puts(text)
ここまでの手順で
- 各単元ごとのWebページ
- 各単元ごとのPDF
- index.html
が出来上がったので、後はこれらをWebサーバー上にデプロイすれば完了です。
デプロイ
当初想定していたよりもかなり複雑になりましたが、デプロイまでの手順をまとめると以下になります。
- マークダウンファイルを作成
- slidevでビルドしてPDFを作成(buildAndCopy.js実行)
- htmlへ変換(convert.rubyを実行)
- 単元毎のリストを作成(createLinkList.jsを実行)
- index.mdを作成(createIndexMd.jsを実行)
- index.htmlを作成(create_index_html.rbを実行)
- 差分をコミットしてリポジトリにプッシュ
- デプロイ
という流れです。現時点ではしばらくGitHub Pagesでの運用を考えているので、プッシュがそのままデプロイに相当します。
デプロイの自動化
この記事を書いている時点ではデプロイを自動化するスクリプトは作っていませんが、さすがに上の手順を毎回手動で実行するのは面倒なので、各種スクリプトの実行とリポジトリへのプッシュまでを自動化するスクリプトの作成を検討中。変換のスクリプトがnodeとrubyで複数言語が混ざっているので、全体を実行するスクリプトを何を使って書くかがベストなのかが悩ましいところですが。
まとめと今後の課題
結果として、パワポを廃止してマークダウンに移行する計画そのものはうまくいったので、結論としてはやってみて良かったです。スクリプトがRubyだったりnodeだったりと、言語がバラバラになってしまったのは複雑さを増す要因になってしまいましたが、いろんな言語を勉強できたという点では良かったとも言えます。ただ、今後の課題も色々あります。
- デプロイの自動化
- スタイルの適用
- index.htmlの柔軟性と拡張性
などなど。この辺りは運用していく中で適宜アップデートを重ねていきます。