Web事業部実績紹介_Webサービス
Web事業部実績紹介_Webサービス
2018.12.15

「Electron」を使って業務改善に役立つツールをつくってみた

まさくに

※この記事はLIGアドベントカレンダー2018の15日目です。

こんにちは。バックエンドエンジニアのまさくにです。

LIGにおいて逃れえぬ仕事のひとつに「LIGブログを書く」という作業があるのですが、僕の場合、この仕事の中に「マークダウンからLIGブログ用のフォーマットを作り出す」という非生産的な作業が含まれています。

僕はたいていの文章をマークダウンで書いていて、このブログ記事も一度マークダウンで書き出してから、WordPressに取り込み、それからLIGブログ用のhtmlに直して公開しているため、この置換作業をこれまでイヤイヤやってきました。

マ ジ な ム ダ。

そこで今回は、マークダウンからLIGブログ用のフォーマットへ変更するためのツールを考えたいと思います。

作りたいものと選定

単純に「マークダウンをhtmlに変更したい」っていうわけではありません。LIGブログには、たとえば下記のようなルールがあります。

  • aタグは内部サイトであれば同一ウィンドウ、外部サイトであれば別ウィンドウ
  • codeタグはpreタグで囲む

どうせならそこまで整形したい。世の中に出ている単純なマークダウンのパーサーではなく、独自でカスタマイズができなければなりません。

また、自分だけが使えるものであれば、たぶん僕の場合、Vim Scriptを書いたり、コマンドで置換したりするのが一番使いやすい気もしますが、どうせ社内でも困ってる人がいそうなものなので、みんなが使えるものがいいでしょう。

そこでElectronを使ってみることにしました。

https://electronjs.org/

前々から触ってみたかったですし、なんか色々そろってそうですし。

ついでに少し調べたところ、markedというマークダウンのパーサーが色々カスタマイズできて便利そうでした。

ただ、そもそもjsを書く機会があまりないんですが、できるのでしょうか。

とりあえず最新のNode.jsを入れる

nvmで最新のNode.jsを入れておきます。

$ nvm ls-remote
# 最新のLTSを確認
# 2018/12/03時点、v10.14.1だったのでインストール
$ nvm install v10.14.1
$ nvm use v10.14.1
Now using node v10.14.1 (npm v6.4.1)
# 使えるようになりました。
$ node --version
v10.14.1

プロジェクトを作る

次にプロジェクトのディレクトリを作ります。

# プロジェクト名はmd2lb(マークダウン to LIG Blog)にしました
$ cd ~/Desktop/works/tmp && mkdir md2lb && cd md2lb

npmでpackage.jsonを対話的に作ります。

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (md2lb)
version: (1.0.0) 0.0.1
description: Markdown to LIG Blog Format
entry point: (index.js) main.js
test command:
git repository:
keywords:
author: masakuni
license: (ISC)
About to write to /Users/ito_masakuni/Desktop/works/tmp/md2lb/package.json:

{
  "name": "md2lb",
  "version": "0.0.1",
  "description": "Markdown to LIG Blog Format",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "masakuni",
  "license": "ISC"
}

Is this ok? (yes) yes

最後にElectron本体と、マークダウンをパースしてくれるmarked、Electronでログ出力するための electron-log 、Electronをビルドするためのelectron-packager もインストールしておきましょう。

この辺はいろいろ進めていくうちに必要になったモジュール群ですが、いまのうちにインストールしてしまいます。

$ npm install --save-dev electron
$ npm install --save marked
$ npm install --save electron-log
$ npm install --save electron-packager

コードを書く

次にコードを書いていきます。

はじめに読み込まれるmain.js

とりあえずこのコードはElectronのQuick Startで書かれていたものと同一です。

const { app, BrowserWindow } = require('electron')

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow () {
  // Create the browser window.
  win = new BrowserWindow({ width: 800, height: 600 })

  // and load the index.html of the app.
  win.loadFile('index.html')

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow()
  }
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

アプリケーションの画面index.html

本当にただのhtmlで、この程度だと特別なことを意識する必要はないのですね。テキストエリアとボタンをひとつ、粗雑に付けてあります。テキストエリアにマークダウンを入れてボタンを押すと、クリップボードにLIGブログのフォーマットでコピーされている、という動作です。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Markdown to LIG Blog Format</title>
    <script src="./js/index.js"></script>
  </head>
  <body>
    <h1>Markdown to LIG Blog Format</h1>
    <div>
        <textarea id="text" rows="8" cols="50"></textarea>
    </div>
    <div>
        <button id="replace">replace & copy</button>
    </div>
  </body>
</html>

画面からイベントを取得するjs/index.js

画面のローディングが終わって、テキストエリアにマークダウンが書かれ、ボタンが押されたとき、markDownのモジュールを実行するようにEventListennerを作っておきます。

'use strict'

const electron = require('electron')
const clipboard = electron.clipboard
const remote = electron.remote
const markDown = remote.require('./lib/markDown')

document.addEventListener('DOMContentLoaded', () => {
  let button = document.getElementById('replace')
  button.addEventListener('click', function () {
    let markdownText = document.getElementById('text').value
    if (markdownText) {
      let html = markDown.toLigBlog(markdownText)
      clipboard.writeText(html)
    }
  })
})

マークダウンからhtmlを返すlib/markDown.js

marked はとても優れたモジュールのようなのですが、LIGブログのフォーマットに合わせる必要があります。Rendererクラスを上書きすることによって、各タグで返す文字列を変更できるようでした。

下記ではhタグとlinkタグ、codeタグをマークダウンからhtmlに取得するルールをカスタマイズしています。

'use strict'

const url = require('url')
const marked = require('marked')
const renderer = new marked.Renderer()

renderer.heading = function (text, level) {
  return `<h${level}>${text}</h${level}>`
}

renderer.link = function (href, title, text) {
  let aTag = ''

  // liginc.co.jp内のリンクなら同一ウィンドウへ、それ以外は別ウィンドウへ
  let parsedUrl = url.parse(href)
  if (parsedUrl.hostname === 'liginc.co.jp') {
    aTag = `<a href="${href}">${text}</a>`
  } else {
    aTag = `<a href="${href}" class="link" target="_blank">${text}</a>`
  }

  return aTag
}

renderer.code = function (code, language) {
  return `<pre><code>
${code}
</code></pre>`
}

const markDown = {
  toLigBlog: function (markdownText) {
    let html = marked(markdownText, {
      renderer: renderer,
      headerIds: false
    })
    return html
  }
}

module.exports = markDown

テスト実行してみる

ここまで書くとElectronのアプリケーションが実行できるようになりました。下記一行をpackage.json内の scripts 内に記載してください。

"start": "electron .",

下記コマンドで実行できるかと思います。

$ npm run start

この上なく雑な画面ですね。

使ってみる

ここでテキストエリアに下記マークダウンを入れて、replace & copy をクリックしてみます。

# h1タグだよー

## h2タグだよー

LIGブログは[ここ](https://liginc.co.jp/)です!

*コードも書ける*

```
if (true) {
    return false;
}
```

そうするとクリップボードに下記文言が入ります。

<h1>h1タグだよー</h1><h2>h2タグだよー</h2><p>LIGブログは<a href="https://liginc.co.jp/">ここ</a>です!</p>
<p><em>コードも書ける</em></p>
<pre><code>
if (true) {
    return false;
}
</code></pre>

改行が……。

この辺は調整が必要そうですが、思い通りのhtml構造にはなりました。

パッケージ化する

最後にElectronをビルドして、自分以外の環境でも使えるようにします。package.jsonに下記一行、やはり scripts の中に記載してください。

"build": "electron-packager . md2lb --platform=darwin --arch=x64 --overwrite",

カレントディレクトリをmd2lbというアプリケーションでビルドするコマンドになります。これを下記のように実行すると、カレントディレクトリに md2lb-darwin-x64 というディレクトリができあがり、その中にアプリケーションが置かれています。これを実行すると、先ほどテスト実行したアプリケーションが立ち上がるはずです。

ビルドのオプションでプラットフォームにlinuxやwinも追加することができます。またアーキテクチャとしてia32などを選べるようですが、対象マシンを今回は必要としないので作っていません。

ちょっとした便利ツールをパッと作れる

正直JavaScriptについては不勉強な部分が多いのですが、簡単にアプリケーションを作ることができました。これは実際便利に使えそうなので、もうちょっと改良すれば役に立てられそうです。

またこういった「ほんのちょっとのことだけど、解決すると少し時短になりそう」というものは、実はみんな困ってそうなので、一度作って配布すれば業務改善になりそうです。実際にこの記事も一度マークダウンで作って、置換してみたものです。

そう考えると解決したいものがたくさんありますね……。2019年は業務改善ができるような年にしたいです。

それでは、まさくにでした。