Help us understand the problem. What is going on with this article?

Puppeteer & Carlo を Markdown スライド作成 CLI ツール (Marp CLI) で活用する

この記事は Speee Advent Calendar 2018 の 10 日目の記事です。先日は @iida-hayato による "AWS Sumerian で AR アプリを作る" でした。

前段

現在、社内で社内ツールの開発/運用にあたる傍らで、 Markdown によるスライド作成ツール Marpの次世代版)をいそいそと開発しています。

Marp (旧版)

遡ること 3 年近く前、 Electron で Markdown プレゼン作成ツールを作って公開するまで という記事を Qiita で公開し、その成果物が Marp でした。

Marp は :star: 累計 7300 over と、国内外問わず多くの方々から支持されており、『ありがとうございます』という気持ちの一方で、『JS の勉強のつもりで作ったのにこんなに伸びちゃった…』という後悔の念もあります。 1

そのため、最近のフロントエンド事情を猛勉強し、ここ 1 年ほどでようやく自在に手足を動かせるようになってきました。

Marp Next (次世代版)

現在、GUI ツールのみならず、 より広い分野で Marp を使ってもらえるようにする ため、Marp の構成要素を 独立したフレームワーク (Marpit)コアモジュール (marp-core) などに分解し、Marp Next として 1 から作り直しています。

正式な発表は、各アプリケーションの準備が整い次第 https://marp.app/ にサイトを立てて行う予定です。この記事は Sneak peek と思ってご覧いただければと思います。

ちなみに、旧 Marp とは互換性が一部失われていますので、お試しの際はご注意ください。

本題

Marp CLI

現在、開発に注力しているのが Marp CLI です。その名の通り、Marp の CLI インタフェース版として開発しているもので、要望がそこそこ多かったこともあり、今年 8 月に最初のプレリリース版をリリースしました。2

Marp Next 向けの Markdown を HTML / PDF に変換する CLI ツールで、Node がインストールされていれば、 npx で以下のようなコマンドを叩けばすぐに使えます。

# HTML に変換
npx @marp-team/marp-cli slide-deck.md
npx @marp-team/marp-cli slide-deck.md -o output.html

# PDF に変換
npx @marp-team/marp-cli slide-deck.md --pdf
npx @marp-team/marp-cli slide-deck.md -o output.pdf

# ウォッチモード(ファイルの変更を監視)
npx @marp-team/marp-cli -w slide-deck.md

# サーバーモード(指定フォルダをローカルで配信)
npx @marp-team/marp-cli -s .

Node が入ってなくても、Docker 経由で使うこともできます。 3

PDF 変換をどうするか

Marp は一貫して、『スライドを PDF に変換して使用する』 というスタンスなので、PDF 変換の機能は必須です。旧版の Marp では、 Electron が提供する API を使用し、内臓の Chromium を使って HTML を印刷する形で PDF を作成していました。

この構成は Marp CLI でも変わりませんが、Electron は GUI ツール向きなので、CLI ツールには (使えはしますが) あまりマッチしません。また、ファイルサイズをかなり喰ってしまうのも課題でした (> 100MB) 。 CLI ツールには軽くあってほしいですよね。

これらの理由から、Marp CLI は 『ユーザーがインストールした Chrome を使って PDF に変換する』 方針を取っています。

Chrome の入っていないユーザーは切り捨てることになってしまいますが、代替策として Docker イメージに Chrome を同梱することで、Docker 経由であれば環境を問わず変換できるようにしました。

Puppeteer

Puppeteer は、(Headless) Chrome および Chromium を操作するための Node API を提供するライブラリです。公式の例にもある通り、HTML :arrow_right: PDF 変換がドえらいカンタンにできてしまいます。

const puppeteer = require('puppeteer')

;(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('https://news.ycombinator.com', {
    waitUntil: 'networkidle2',
  })
  await page.pdf({ path: 'hn.pdf', format: 'A4' })

  await browser.close()
})()

https://github.com/GoogleChrome/puppeteer#usage

puppeteer-core

実際の Marp CLI では、今年 8 月から提供が開始された puppeteer-core を使用しています。

Puppeteer はデフォルトで、npm install puppeteer を実行した際に、バージョンに沿って最適な Chromium をダウンロードし、それを使用してくれます。この挙動自体は問題ないのですが、先に挙げた『ファイルサイズを喰ってしまう』という課題が引き続き残ります (> 300MB) 。

その点 npm install puppetter-core は、Chroimum の自動インストールを行わないため、ディスク容量の浪費の心配がありません。操作する Chrome (Chromium) は、Puppeteer でブラウザを起動する際に自分で指定する必要があります。

サンプル

現行の Marp CLI から、PDF 変換に関するコードを抜き出してみました。変換対象として、https://yhatt-marp-cli-example.netlify.com/ でホストしている、Marp CLI の HTML スライド例を使用しています。

npm i chrome-launcher puppeteer-core is-wsl を実行後、Node で以下を実行すると、output.pdf にスライド PDF が出力されます。

const chromeFinder = require('chrome-launcher/dist/chrome-finder')
const puppeteer = require('puppeteer-core')
const isWSL = require('is-wsl')

;(async () => {
  // Chrome を開く
  const browser = await (async () => {
    // 起動する Chrome の設定 (後述)
    const args = ['--enable-blink-gen-property-trees']
    let finder = isWSL ? chromeFinder.wsl : chromeFinder[process.platform]

    if (process.env.IS_DOCKER) {
      // Docker 環境の設定 (後述)
      args.push('--no-sandbox', '--disable-dev-shm-usage')
      finder = () => ['/usr/bin/chromium-browser']
    }

    // Puppeteer で Chrome を起動
    return await puppeteer.launch({
      args,
      executablePath: finder ? finder()[0] : undefined,
    })
  })()

  try {
    // タブを開く
    const page = await browser.newPage()

    try {
      // スライドの HTML にアクセス
      await page.goto('https://yhatt-marp-cli-example.netlify.com/', {
        waitUntil: ['domcontentloaded', 'networkidle0'],
      })

      // PDF に変換 (後述)
      await page.pdf({
        path: './output.pdf',
        printBackground: true,
        preferCSSPageSize: true,
      })

      console.log('Success!')
    } finally {
      await page.close()
    }
  } finally {
    await browser.close()
  }
})()

コードのボリュームは増えましたが、流れは公式のサンプルとそれほど変わりません。

起動する Chrome の設定

puppeteer-core では executablePath でパスを指定する必要があります。Chrome のパス解決ロジックを自前でメンテナンスするのも大変なので、Chrome 公式が提供する別プロジェクト chrome-launcher のものを拝借しています (chrome-finder.ts) 。

is-wsl による判定処理は、WSL (Windows Subsystem for Linux) 環境で Windows の Chrome を使用するための分岐です。

--enable-blink-gen-property-trees

コレはかなり新しめのフラグです。Marp Next のベースとなるフレームワーク Marpit では、SVG タグでスライドを描画する Inline SVG slide というモードがあり、Marpit の高度な機能を活かし切るのに欠かせないモードになっています。(高度な背景レイアウト、JavaScript 不要、Markdown 本来の DOM 構造を壊さない etc)

しかし、このレンダリングは Firefox がめっぽう安定している一方で、Chrome にはバグが結構あります(スクロールバーが出るとスケーリングがおかしくなったり、ビデオコンポーネントが中にあるとレイアウトが崩れたりする)。このフラグである程度解消されるため、レンダリングバグを避けるために ON に設定しています。

同じように、Blink 派生元の WebKit でも 10 年来の未解決問題となっており、Safari 環境でのスライド表示には polyfill を作って対策しています。 JS が必須になってしまうのが痛い...

Docker 環境向けの設定

前述の通り、Marp CLI は Docker 環境で Chrome による変換ができますが、Puppeteer をコンテナ内で動かす際はいくつか注意点があり、ドキュメント でも言及されています。

--disable-dev-shm-usage は、共有メモリ (/dev/shm) への書き込みを回避するためのオプションです。デフォルトの共有メモリの容量は Chrome を動かすには小さすぎる (64MB) ため、このオプションを指定することで大きなページでのクラッシュを回避します。

PDF 変換時のオプション

設定したのは以下の 2 つです。

  • printBackground: 背景画像を印刷するかどうか
  • preferCSSPageSize: CSS の @page で設定したサイズを使用するかどうか

読んで字の如くですね。

出来上がった PDF

スクリーンショット 2018-12-10 9.50.04.png

再現性もバッチリです。

ということで、CLI 環境に適した PDF 変換処理が無事実現できました。:ok_hand:


プレビュー機能をつけたい

話はちょっと変わりまして、Marp CLI には最初に示した通り、ウォッチモード (--watch, -w) やサーバーモード (--server, -s) があります。

変換した HTML をブラウザに表示すれば、Markdown の保存を検知してリロードしてくれるので、お好みのエディタでスライドが書けます。4

そうなると、Deckset のように、プレビューできる小窓も欲しくなってくるかもしれません。

Deckset

Features – Deckset for Mac

というわけで、CLI からプレビュー小窓を表示する機能の実装もはじめました。 (今のところ Experimental です)

Carlo

プレビュー機能の実装にあたり、Carlo を使ってみました。

Carlo は、先月リリースされたばかりの、Puppeteer + Chrome による Node アプリケーションフレームワークです。実は、これも puppeteer-core でインストール済みの Chrome を使用する構成で、全く同じ構成を持つ Marp CLI にスッと導入できました。

今回の導入例は、以下の Qiita 記事 で解説されているユースケースそのままですね。

所感として、ガッツリとしたデスクトップアプリを作るというよりも、CLI として提供していたような Node.js のツールに対して、少しだけ GUI を足したい、みたいなケースに一番向いてそうです。

Carlo で Node.js のツールに GUI をもたせる - Qiita

:warning: 2020/03/03 追記:

残念ながら、現在 Carlo はもうメンテナンスされていません。元の開発者が Google を辞めてしまい、Chrome チームもメンテナンスに時間を取れないとのことです。そのため、現状は新たに Carlo を利用することはあまりお勧めしません。

Marp CLI でもこの状況を見越してずっと Expereimental のままにしてあったので、メンテナンスされなくなった今、実装した機能をどう対処すべきかは改めて検討中です。(利用頻度が少ないと判断した場合、削除される可能性もあります)

使い方

すでにサーバーの実装は済んでいるため、やることはローカルサーバーを表示させることだけです。拍子抜けするほど簡単でした。5

const carlo = require('carlo')

;(async () => {
  const app = await carlo.launch({
    channel: ['canary', 'stable'],
    title: 'Marp CLI',
    args: ['--enable-blink-gen-property-trees'],
  })

  // ローカルサーバーを表示
  await app.load('http://localhost:8080/')
})()

carlo の API は非常にシンプルで、Puppeteer の薄いラッパーとなっているため、Puppeteer が分かればすぐ理解できます。

複数ウィンドウを使用する場合
const win = await app.createWindow({
  height: 480,
  width: 640,
})

await win.load('https://yhatt-marp-cli-example.netlify.com/')

ちょっとクセがあり、引数を省略した app.createWindow() を使うと、タブバーやアドレスバーのついた、良く見慣れた Chrome ウィンドウになるようです。サイズや位置の指定があると、フレームのみのウィンドウになります。

Marp CLI で使う

まだまだ実験中なので、サーバーモードでのみ対応しています。 普通のファイルでもプレビューできるようになりました。

npx @marp-team/marp-cli --preview -s .

利用イメージ

marp-cli-cast.gif

Vim 側で編集した内容を保存すると、その内容がプレビューウィンドウにもちゃんと反映されます。 :ok_hand:

ちなみに、このウィンドウを最大化 or フルスクリーン表示すれば、そのままプレゼンモードとして使うこともできます。今後プレゼンテーション関連機能は拡充していくつもりですが、待ちきれない方はこちらをお使いください。:bow:

終わりに

PuppeteerCarlo の採用例として、Markdown スライド作成 CLI ツール (Marp CLI) での活用についてご紹介しました。

Marp CLI は、年内にはドキュメントを整備し、プレリリースを脱せるように鋭意開発中です。Marp Next は、PWA を採用した Web 版も技術検証段階 に入っていますので、続報をお待ちいただければ幸いです。

明日は @ryukku-n です。

追記: 2018/12/23

その後、マイナーアップデート Marp CLI v0.1.0 をリリースし、無事年内にプレリリースを卒業しました :tada:


  1. CoffeeScript/jQuery という古い構成と、テストが皆無だった(当時は JS のテストの書き方すら知らなかった)のが継続的なメンテナンスの足を引っ張りました。 

  2. 他にも、Marp が著名になって以降、『好みのエディタで書きたい』『Vim のキーバインドで書きたい』といった声が非常に多かったのも理由です。そのため、現在はエディタを自前で作るよりも、コンバーターの充実に注力しています。 

  3. Docker 対応も寄せられていた要望です。 

  4. 変更の検知についても要望に上がっていたものの、ずっと後手に回っていました。。 

  5. 現状の Marp CLI では、私の理解不足もあり若干回りくどい書き方になっていますが、特にこんなことしなくても大丈夫です。 

speee
株式会社Speeeは「解き尽くす。未来を引きよせる。」というミッションを実現すべく、中長期的な目線で企業価値を最大化させていくため、組織・事業のStyleを大切にした永続的な価値創造を目指しています。
https://www.speee.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away