🌐

sb2re-online: 小さいWebツールをDenoで作ってGitHub Pagesに公開してみた

2022/07/31に公開

TL;DR

  • Scrapbox記法の文章をRe:VIEW記法へ変換してくれるWebアプリOnline ScrapBox to Re:VIEW Converter (sb2re-online) を作りました。
  • Deno向けのバンドルツールpackupでビルドし、GitHub Pagesで公開しています。

https://twitter.com/kn1cht/status/1538518222755942402

kn1cht.github.io/sb2re-online/で使えます。

https://kn1cht.github.io/sb2re-online

リポジトリはこちらです。

https://github.com/kn1cht/sb2re-online

この記事で得られるかも知れない情報

  • Deno製のコマンドラインツールをWebアプリ化する方法
    • sb2re-onlineはブラウザだけで動く小さいアプリなので、バンドルツールのpackupで静的ファイルをビルドしています
    • DenoのWebアプリをデプロイする方法としてはDeno Deployが有名ですが、サーバーサイドがいらない用途だったため、公開にはGitHub Pagesを使いました
    • 内部で呼び出したツールが出してきたエラーを、ページに分かりやすく表示する仕組みも入れました

モチベーション

筆者が所属する同人サークル「東京大学きらら同好会」では、複数人で制作する同人誌の原稿を、WikiツールのScrapboxで執筆しています。
また、合同誌として組版する段階では組版ソフトウェアRe:VIEWを使用しています。

Scrapboxは独自のマークアップ形式を採用しており、Re:VIEWで使用するためにはフォーマットの変換が必要です。この目的のため、サークル会員のふぁぼん氏がScrapbox記法からRe:VIEW記法への変換ツールsb2reを実装してくれました。

https://twitter.com/syobon_hinata/status/1471864549720883204

sb2reはDenoで作られていて、deno installでインストールすれば気軽に使えます。ただ、「Denoをインストール→sb2reをインストール」という手順をいちいち踏むよりも、ブラウザだけでいきなり使えたほうがさらにさらに気軽でしょう。

sb2re-onlineの画面

というわけで、sb2re-onlineができました。左のボックスにScrapboxのテキストを貼り付けると、右のボックスにリアルタイムでRe:VIEWへの変換結果が出てきます。

技術選定と初期の実装

sb2reがDeno製なので、ブラウザ版もDenoで作りたいな~ということでDenoでフロントエンド開発する方法を調べました。なお、筆者はDeno初体験のNode.jsキッズです。

ビルドツール(packup)

Denoで使えるWebフレームワークはかなりいろいろな種類が出てきています。今回の用途では処理がブラウザだけで完結するので、なるべくシンプルにフロントエンドだけを扱うものがいいと思いました。

https://scrapbox.io/uki00a/Denoのフロントエンドフレームワークの比較

というわけで選んだのがpackupです。設定ファイルいらずで、エントリポイントのindex.htmlを渡せばビルドしてくれるシンプルさに惹かれました。

https://zenn.dev/kt3k/articles/1df2e54cd9d4f3

React

フロントエンドのライブラリにはReactを使いました。Deno世界ではSkypack経由でimportできます。
textareaの更新を検知するためにReact Hook Formも入れておきました。

app.tsx
import React from "https://cdn.skypack.dev/react@18.1.0?dts";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18.1.0?dts";
import { useForm } from "https://cdn.skypack.dev/react-hook-form?dts";

sb2re

Scrapbox→Re:VIEW変換ツールsb2reをそのまま使います。
Deno同士なのでそのままGitHubの.tsファイルをimportしてくれば完了です。Deno最高!

app.tsx
import scrapboxToReView from "https://raw.githubusercontent.com/fabon-f/sb2re/master/mod.ts";

実装

実装といっても、textareaに入ってきた文字列をsb2reに投げつけて結果を表示させるだけなので、新規に書くことはほとんどありません。

app.tsx
function App() {
  const { register, watch } = useForm();
  const watchInput = watch("input", sampleTxt);
  return (
    <div id="app">
      <div class="nav">
        <h1>sb2re-online: Online Scrapbox to Re:VIEW Converter</h1>
      </div>
      <div class="editor">
        <div class="editor_wrapper">
          <textarea value={watchInput} class="input" {...register("input")}></textarea>
        </div>
        <div class="editor_wrapper">
          <textarea value={scrapboxToReView(watchInput.trim())} class="output"></textarea>
        </div>
      </div>
    </div>
  );
}

addEventListener("DOMContentLoaded", () => ReactDOM.render(<App />, document.querySelector("#main")));

なんとメイン部分20行くらいで立派なWebアプリが完成しました(index.htmlstyle.scssは別)。Deno最高!(2)

ビルド

packup build index.htmlするとdist/以下にHTML, JS, CSSが書き出されます。

公開

Deno製Webアプリのデプロイ先としてはDeno Deployがよく使われている印象です。ただ、sb2re-onlineは静的ファイルをブラウザに読ませるだけでOKなので、サーバーサイドの機能までは必要としていません。

そこで、packupのビルド結果をGitHub Pagesにデプロイすることとしました。
GitHub Actionsでpackup buildし、peaceiris/actions-gh-pagesによってdist/gh-pagesブランチにコミットする仕掛けです。

.github/workflows/gh-pages.yml(抜粋)
    steps:
      - uses: actions/checkout@v2
      - uses: denoland/setup-deno@v1
      - name: Install packup
        run: deno run -A https://deno.land/x/packup@v0.1.13/install.ts
      - name: packup build
        run: packup build index.html
      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        if: ${{ github.ref == 'refs/heads/main' }}
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

ツール側が出してきたエラーをWebページに表示させる

メイン部分はここまでで完成なので余談です。

sb2reはScrapbox記法をパースしてRe:VIEWに変換しています。その途中でエラーが起きると、例外を投げて止まる場合がありました。

コマンドラインツールなら「じゃあ直して再実行しようか」で済む話です。
一方、Reactで特に対策なくエラーが発生すると、何の前触れもなく画面が真っ白になって操作不能になります。ユーザはエラーの原因が分からないままページを再読み込みするしかありません。
さらには、sb2re-onlineでは文字を入れた瞬間にsb2reが実行されるため、エラー原因を特定するまで何度もWebアプリが停止してしまいます。

これはユーザ体験が悪すぎるということで、try-catchでエラーを受け取ってreact-notificationsへ渡すようにしました。

app.tsx
import { ReactNotifications, Store } from "https://cdn.skypack.dev/react-notifications-component?dts";

// (中略)

function executeSb2re(watchText: string, option: {}) {
  let converted = "";
  try {
    converted = scrapboxToReView(watchText, option);
  } catch(e) {
    Store.addNotification({
      title: "Please fix your Scrapbox syntax",
      message: e.toString(),
      type: "danger",
      container: "bottom-left",
      dismiss: {
        duration: 6000,
        onScreen: true,
        showIcon: true,
      },
    });
  }
  return converted;
}

これで、エラーになるような構文が入力されてもWebアプリは落ちず、さらに通知メッセージからエラー原因を推定できるようになりました。

Error Notification

また、以前のsb2reではエラーを示す方法としてthrow new Error()console.error()が混在していました。後者はブラウザの開発者ツールを開かなければ見ることもできないので、作者のふぁぼん氏に相談したら、エラー時に呼ばれる関数を外から指定できるオプションを用意してくださいました。感謝――。

まとめ

Scrapbox記法からRe:VIEW記法への変換を気軽にできる小さなWebツールsb2re-onlineをDenoで制作しました。

https://kn1cht.github.io/sb2re-online

「Denoは開発体験がいいぞ」と色んなところで耳にしていたとおり、ミニマルな実装の完成までがたいへんスムーズで驚きでした。これ試したいな、と思ったときにCDNやGitHubのURLをちょろっと書くだけで取得してきてくれるのは楽しいですね。
今後もDenoでなにか作ってみたくなりました。

宣伝

Scrapbox, Re:VIEW, sb2re-onlineを活用して制作したまんがタイムきららの合同誌「Micare vol. 2」が、コミックマーケット100 土曜日東地区Y16a「東京大学きらら同好会」で頒布されます! 夏コミにお越しの方はぜひ!!

https://twitter.com/UTKiraraCircle/status/1535190931204624385

GitHubで編集を提案

Discussion