Electron で Markdownプレゼン作成ツールを作って公開するまで

Markdown でスライドを作るためのツールを漁っていたのですが、なかなか自分好みのものが無く、Electron の勉強を兼ねて作ってみた話です。

> <ins>追記 (2018/12/10):</ins> その後の進捗はこちら
> :arrow_right: **[Puppeteer & Carlo を Markdown スライド作成 CLI ツール \(Marp CLI\) で活用する - Qiita](https://qiita.com/yhatt/items/874d5bfa919c32728403)**

## 成果物

今回作ったプレゼンツールは、以下にてプレリリース版を公開しています。

**Marp - Markdown presentation writer**
**https://yhatt.github.io/marp/** *(旧称 mdSlide)*

[![Marp](https://qiita-image-store.s3.amazonaws.com/0/70467/169b394c-e43a-3c84-96da-f84bfd69e43a.png)
](https://yhatt.github.io/marp/)

### 追記 (2016/07/13 19:42)

＿人人人人人人人人人人人人人人＿
＞　突然のGithubトレンド入り　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y￣

![](http://cdn-ak.f.st-hatena.com/images/fotolife/y/yuki-hattori/20160714/20160714202442.png)

## Markdown スライドツール比較表

|私の欲しい機能 ▼|[Deckset](http://www.decksetapp.com/)|[reveal.js](http://lab.hakim.se/reveal-js/#/)|[md2key](https://github.com/k0kubun/md2key)|[Pandoc](http://pandoc.org/)|[mdslide](https://github.com/ymrl/mdslide)|[Qiita](http://blog.qiita.com/post/146195241579/slide-mode)|
|---:|:---:|:---:|:---:|:---:|:---:|:--:|
|**Markdown**|:o:|:o:|:o:|:o:|:o:|:o:|
|**PDF変換**|:o:|印刷経由|Keynote|:o:|:x:|:x:|
|**ライブプレビュー**|半自動|手動|:x:|:x:|:x:|:o:|
|**無料**|3600円|:o:|:o:|:o:|:o:|:o:|
|**クロスプラットフォーム**|Mac|:o:|Mac|:o:|:o:|:o:|

Electron による開発を選んだのは、**クロスプラットフォームで、環境に依存せずPDFが作れる仕組みがある ([printToPDF メソッドがある](https://github.com/atom/electron/blob/master/docs/api/web-contents.md#webcontentsprinttopdfoptions-callback))** のが一番大きいです。1度PDFになれば 可搬性と汎用性が抜群ですし。

reveal.js はかなり惜しいんですが、印刷経由なので、環境によってはPDF変換ツールが必要になってしまうのが…。

## まず Markdown エディタを作る

スライドの前に、まずは Markdown エディタを作る所から始まるわけですが、ここは先人の知識を多分に拝借しました。ありがとうございます :bow:

**Electron/Node.js初心者がマテリアルデザインなMarkdownエディタを作ってみた**
**http://qiita.com/hoshi-k/items/565acf909d46c0c5c28b**

### 今回の構成

| |採用したもの|備考|
|---:|:---:|---|
|**JavaScript**|[CoffeeScript](http://coffeescript.org/) / [jQuery](https://jquery.com/)|スピード重視で、慣れてるもので|
|**CSS**|[Sass](http://sass-lang.com/)|今後のスライドテーマ展開を見据えて|
|**UI/CSS Framework**|[Photon](http://photonkit.com/)|良い感じのMacネイティブルック|
|**エディタ部分**|[CodeMirror](https://codemirror.net/)|　|
|**Markdownパーサー**|[markdown-it](https://github.com/markdown-it/markdown-it)|スライド記述用に設定する（後述）|
|**Syntax highlight**|[highlight.js](https://highlightjs.org/)|　|
|**絵文字表示**|[twemoji](https://github.com/twitter/twemoji)|　|

## Markdown パース結果をスライドにするための工夫

### ページ区切りを `---` にする

今回、Deckset 式の `---` 区切りでの改ページを採用しました。

```markdown
# Slide 1

hogehoge

---

# Slide 2

fugafuga
```

その際に、スライドプレビュー等の表示をコントロールするため、レンダリング結果がページごとの `block` 要素になってると嬉しいなーと思いまして、パーサーの設定をややいじりました。

```coffeescript
md.renderer.rules.hr = (token, idx) ->
    '</div><div class="slidePage">'

rendered = "<div class=\"slidePage\">#{md.render(markdown)}</div>"
```

ちょっとローテクですが、`<hr />` を出力する部分を `<div>` の終了要素+開始要素 のペアに変えて、レンダリング結果全体を同様の要素で囲みます。

```html:rendered
<div class="slidePage">
    <h1>Slide 1</h1>
    <p>hogehoge</p>
</div>
<div class="slidePage">
    <h1>Slide 2</h1>
    <p>fugafuga</p>
</div>
```

今回作ったツールは、作成した Markdown パースクラスで markdown-it をラップし、この設定等を行っています。

### スライドPDFを作成する

Electron で スライドPDFを作成するためには、

1. `<webview>` にMarkdownをレンダリングして、
2. `webView.printToPDF` で印刷の結果をPDFに変換

という感じになります。なので、スライドになる部分のCSSは `@media print` メディアクエリで制御すればよさそうです。

```css
@media print {
  /* ... */
}
```

改ページ等はどうするのか？と思っていたんですが、そのものズバリな設定がCSSにありました。

```css
body {
    page-break-inside: avoid;
}

.slidePage {
    width: 297mm;
    height: 210mm;
    overflow: hidden;

    /* 改ページ設定 */
    page-break-after: always;
}
```

`page-break-inside: avoid;` で、`body` 要素内での改ページを避け、`page-break-after: always;` で各スライドの後で確実に改ページする設定にすることで、1ページ1スライドを実現させました。

今回作ったアプリでは ライブプレビュー実装の関係もあり、実際には Sass や JS, CSS Variables も使って、もう少し細かく制御しています。


## クロスプラットフォームで注意すること

クロスプラットフォームでネイティブルックなアプリを作る際には、OSの特性を知らないとハマリそうです。細かい機能については [Electron のドキュメント](https://github.com/atom/electron/blob/master/docs-translations/jp/tutorial/desktop-environment-integration.md) を参照することにして、私がハマったポイントをいくつか挙げておきます。

### メニュー

Windows / Linux ではメニューは**ウィンドウ**に紐づきますが、Mac では**アプリケーション**に紐づきます。

Electron の [menu API ドキュメント](https://github.com/atom/electron/blob/master/docs-translations/jp/api/menu.md) では、ウィンドウに紐づく時のサンプルとして以下のように書かれています。

```javascript
{
  label: 'Reload',
  accelerator: 'CmdOrCtrl+R',
  click: function(item, focusedWindow) {
    if (focusedWindow)
      focusedWindow.reload();
  }
}
```

これを参考に、「開く」コマンド実装時、「現在開いているウィンドウのエディタに対し、ファイルの内容を読み込む」という感じで処理を書いていました。
その結果、[Mac 環境で ウィンドウが無い状態から「開く」が出来ないバグを引き起こしました…](https://github.com/yhatt/mdslide/issues/7) :sweat:

`focusedWindow` に window が入ってこないため、この場合は 新規にウィンドウを作る処理を「開く」コマンドの処理に含める必要があります。

### ファイルの関連付け

Windows や Linux の場合の関連付けはシンプルで、`process.argv` で得られる引数にファイルパスが入ってくるので、それを解釈して 開く処理を書けばOKです。

Mac の場合は、そのようなケースもありますが、主に [open-file](https://github.com/atom/electron/blob/master/docs/api/app.md#event-open-file-os-x) イベントの発火がメインです（アイコンにファイルが D&D されたりした時に呼び出されます）。メインプロセスで、以下の例のように、開く処理を呼び出します。

```coffeescript
app.on 'open-file', (e, path) ->
  e.preventDefault()
  
  # パスを開く処理を呼び出す
  MdsWindow.loadFromFile path
```

※ `MdsWindow` は、Electron の window をラップしたクラスです。

#### Mac: どのファイルを関連付けるか

ただし、Macの場合これを設定しただけでは D&D してもシカトされてしまいます。Mac の場合、アプリケーションがどのファイルタイプに関連付けられるのかを設定する必要があります。

Marp では gulp タスク内でのビルドに [electron-packager](https://github.com/maxogden/electron-packager) を使用していますが、その処理後に `***.app/Contents/Info.plist` の設定を以下のように更新しています。

```js
{
  CFBundleDocumentTypes: [
    {
      CFBundleTypeExtensions: ['md', 'mdown']
      CFBundleTypeIconFile: ''
      CFBundleTypeName: 'Markdown file'
      CFBundleTypeRole: 'Editor'
      LSHandlerRank: 'Owner'
    }
  ]
}
```

各パラメータについて、私は以下を参考にしました。iOSアプリ向けの解説ですが、仕組みは同様です。

> Safx: iOSアプリケーションを特定のファイル形式に対応させる方法について
> http://safx-dev.blogspot.jp/2010/11/ios_21.html

#### Win: 関連付けとジャンプリスト

似たようなケースは Windows にもあり、[ジャンプリストにファイル履歴を表示させる](https://github.com/atom/electron/blob/master/docs-translations/jp/tutorial/desktop-environment-integration.md#windows-%E7%95%99%E6%84%8F%E7%82%B9) 場合、システム上でアプリケーションとの関連付け設定がされている必要があります。

![](https://camo.githubusercontent.com/3310597e01f138b1d687e07aa618c50908a88dec/687474703a2f2f692e6d73646e2e6d6963726f736f66742e636f6d2f64796e696d672f49433432303533382e706e67)

[インストーラー等で関連付けを実施する](https://github.com/theodo/electron-boilerplate/blob/master/doc/file-association-in-windows.md) のが王道パターンだと思いますが、ユーザーが「プログラムから開く」等で指定しても、ジャンプリストには現れる模様です。

###### 参考

> ファイル拡張子とプログラムの関連付け - Web/DB プログラミング徹底解説
> http://keicode.com/windows/associate-app-with-file.php

## 公開

**yhatt/marp: Markdown presentation writer, powered by Electron.**
**https://github.com/yhatt/marp/**

開発およびリリースは 原則 Github上で行い、[配布ページ](https://yhatt.github.io/mdslide/) には [Github Pages](https://pages.github.com/) を使用しました。

以前は Github が用意していたテンプレートを使用していましたが、現在は Jekyll + Bootstrap で、テンプレートも自ら作成しています。プラグイン等を使用しないのであれば、ローカルに用意した Jekyll の動作環境をそのままコミットすれば Github が Jekyll でホスティングしてくれるので、ラクチンです。

このツールではまだ実現できていませんが、もっとしっかりアプリケーションとして整備するためには、

- アプリケーションへのコードサイニング (Win, Mac)
- 自動アップデートの仕組み

などを整えなければなりません。このあたりは  [ヌーラボさんのTypetalkの例](https://nulab-inc.com/ja/blog/typetalk/3-points-for-releasing-production-electron-app/) が参考になります。

> 1. 各種アイコンを用意する
> 2. コードサイニングを行う
> 3. AutoUpdaterに対応する
>
> -- *Electronアプリをプロダクトとして「正しく」リリースするために必要な3つのこと - ヌーラボ [Nulab Inc.]*
> *https://nulab-inc.com/ja/blog/typetalk/3-points-for-releasing-production-electron-app/*

## まとめ

ということで、Electron でキチンとクロスプラットフォームなツールを作って公開してみて感じたのは以下です。

- **作ること自体は結構カンタン** (node 初心者の私でも 5日ぐらいで初期バージョンができた)
- *いざ公開するとなると、OS別のいろんなケースを考えないとハマる可能性が高い*
    - WebView で完結するアプリならそうでもないのかも？

ここまでしっかり公開すると、*「ちゃんと整備しないと!!!」*という気持ちに否が応でもなるので、自分に負荷を課すつもりでアプリを作ってみるのはアリだと思います。

### ツールの展望

実際に Marp (旧称 mdSlide) を使って作成したスライドPDFは、[SlideShare](http://www.slideshare.net/ssuser1958e2/mdslide-example) にて見られます。

[![marp-ss.png](https://qiita-image-store.s3.amazonaws.com/0/70467/430becb1-3112-e276-89bc-9aeb48867e8b.png)
](http://www.slideshare.net/ssuser1958e2/mdslide-example)

Electron には フレームレスなウィンドウを作成したり、外部ディスプレイの情報を得るAPIもありますので、ゆくゆくは Marp 単体でのプレゼンとか、発表者ツール機能とかができるといいですね。

### 2016/06/03 補足：`printToPDF` で出力サイズをカスタマイズできるようになった

Marp で PDF 出力の肝になる `webContents.printToPDF` メソッドですが、かつての Electron (<= 1.2.0) では[出力できるサイズが限られていました](https://github.com/electron/electron/blob/c1272743b922d8f2f2990977a863c5776c4c36cf/docs/api/web-contents.md#webcontentsprinttopdfoptions-callback)。
そのため、PowerPoint や Keynote 互換の 4:3 サイズのPDFは作成できませんでした。

言うほどたいした問題ではないのですが、とある環境で Marp で作ったスライドを画面に映し出したら、他のスライドに比べて、縦横比が微妙にズレを起こしていたなんてことも...。

しかし Electron 1.2.1 で `printToPDF` に[任意のサイズを渡して出力できるようになった](https://github.com/electron/electron/pull/5812)ようです。ミクロン単位 (1000micron = 1mm) でサイズを指定できます。

```js
win.webContents.printToPDF({
  pageSize: {
    width:  254000, // 960px
    height: 190500  // 720px
  }
}, (error, data) => {
  // ...
});
```

[1.2.1 のリリースノートには書かれていない](https://github.com/electron/electron/releases/tag/v1.2.1)ので、まだ実験的な機能のようですが、今のところ 4:3 も 16:9 もバッチリ出力できているので、似たようなお悩みのある方はお試しください。

## 2016/07/17追記：掲載歴

[会社のブログにも少し裏話を書きましたが、](http://technica-blog.jp/entry/2016/07/14/204951) 他にもいろんなサイトに紹介していただいたので、一部紹介。

### Hacker News

> **Marp: Markdown Presentation Writer | Hacker News:**
> **https://news.ycombinator.com/item?id=12079671**
>
> [![](http://cdn-ak.f.st-hatena.com/images/fotolife/y/yuki-hattori/20160714/20160714202439.png)](https://news.ycombinator.com/item?id=12079671)

ここに取り上げられただけで、各方面から普段の200倍ぐらいのアクセスが来て、嬉しい悲鳴を上げています。

### GIGAZINE

> **無料でプレゼンのスライドをマークダウン記法で作成できる「Marp」**
> **http://gigazine.net/news/20160713-marp/**
>
> [![](http://cdn-ak.f.st-hatena.com/images/fotolife/y/yuki-hattori/20160714/20160714202748.png)](http://gigazine.net/news/20160713-marp/)

個人の勉強のつもりが、こんな著名サイトでご紹介いただけるとは...！
マニュアル化してなかった機能までフォローしているところが凄い。(画像D&D)