Excelのシートの内容をjavascriptでjsonに変換してpug用のファイルとして出力したメモ

## 動機

ホームページ作成にpugを使うようになったが、表を作成するためのjsonを手打ちするのが面倒になってきた。
Excelで作成した表をコマンド一発で取り込めるようにする。

## 動作環境

* windows10
* Excel 2016 MSO(16.0.9001.2080) 32bit
* vagrant1.9.7
* virtualbox5.1.26
* ubuntu-16.04
* Docker version 17.09.0-ce, build afdb6d4
* docker-compose version 1.17.1, build 6d101fb

## フォルダ構成

```yaml
- bin
  - excel-to-data.sh
- docker
  - excel
    - app.js
    - Dockerfile
  - docker-compose.yml
- data
  - chart.js
- htdocs
  - pug
    - rulebook.pug
    - includes
      - common
        - chart.pug
```

[最終的なソース](https://github.com/hibohiboo/whiteMapTRPG/tree/f9a9be65ead80ea075c02da027a2f39d24a48949/)

## docker設定

```docker:docker/excel/Dockerfile
FROM node:9.5.0

# コンテナ上の作業ディレクトリ作成
WORKDIR /app

# 後で確認出来るようにpackage.jsonを作成
RUN npm init -y

# エクセルを扱う
RUN npm i -D exceljs
```
```javascript:docker/excel/app.js
const fs = require('fs');
const excel = require('exceljs');

(async _ =>{
  // コンテナ上のパスを記述
  const src = `/app/data/chart.xlsx`;
  const dest = `/app/dest/chart.pug`;

  // エクセルの内容を取得
  const sheets = await getSheets(src);

  // 書き出すファイルの内容
  let content = ``;

  // シート名を変数名、変数の値をレコードとして、pugファイルの内容を作成
  sheets.forEach(sheet=>{
    content += `- ${sheet.name}=${JSON.stringify(sheet.records)}\n`;
  });

  // ファイルに書き出し
  fs.writeFile(dest, content, (err)=>console.log(err));

  console.log(`Excel: ${src} to Pug: ${dest} `);
})();

/**
 * エクセルファイルを読み込み、シートごとのデータの配列を返す
 * @param {*} filepath 
 * @returns シートごとのデータの配列
 */
async function getSheets(filepath){
  const dataArr = [];
  const workbook = await readWorkbookAsync(filepath);
  workbook.eachSheet(function(worksheet, sheetId){
    const data = getSheetData(worksheet);
    dataArr.push(data);
  });
  return dataArr;
}

/**
 * シートのデータを取得
 * @param {*} sheet 
 * @returns {name:"シート名", records: [1行目をプロパティ名としたオブジェクトの配列]}
 */
function getSheetData(sheet){
  let columIndex=1;
  let rowIndex=1;
  const headerRow =[];

  // 1行目をヘッダとする
  while(sheet.getCell(1, columIndex).value !== null){
    headerRow.push(sheet.getCell(1, columIndex++).value);
  }

  const columnCount = headerRow.length;
  const dataArr = [];

  // 二行目からのデータを取り込み
  rowIndex++;
  while(sheet.getCell(rowIndex, 1).value !== null){
    const data ={};

    // ヘッダをプロパティ名として値を設定
    for(let col = 0; col < columnCount; col++){
      data[headerRow[col]] =sheet.getCell(rowIndex, col + 1).value;
    }

    dataArr.push(data);
    rowIndex++;
  }
  
  return {name: sheet.name, records: dataArr};
}

/**
 * エクセルファイルを非同期で読み込む
 * @param {*} filePath 
 */
function readWorkbookAsync(filePath){
  return new Promise((resolve, reject) => {
    const workbook = new excel.Workbook();
    workbook.xlsx.readFile(filePath).then(()=>{
      resolve(workbook);
    });
  });
}
```
```yaml:docker-compose.yml
version: '3'
services:
  # エクセル取込
  excel:
    build: ./excel
    volumes:
      - ./excel/app.js:/app/app.js
      - ../data:/app/data
      # pugの変数定義ファイルに書き出し
      - ../htdocs/pug/includes/common:/app/dest
```

## 実行

```bash:bin/excel-to-data.sh
#!/bin/bash

# このシェルスクリプトのディレクトリの絶対パスを取得。
bin_dir=$(cd $(dirname $0) && pwd)

cd $bin_dir/../docker && docker-compose run excel /bin/bash -c "node app.js"
```

## エクセルファイル

* data/chart.xlsxを読み込む
* シート名を変数名とする
* 1行目をヘッダとして、オブジェクトのプロパティとする。

![2018-02-10.png](https://qiita-image-store.s3.amazonaws.com/0/99876/843a7df0-ce53-37cb-70a6-aab7a80327ee.png)

## 出力結果

```pug:htdocs/pug/includes/common/chart.pug
- classList=[{"name":"ビッグ","image":"big.png","description":"体が大きいことを表すクラス。恵まれた体格を活かしたアビリティを習得できる。チビと同時に選ぶことはできない。"},{"name":"チビ","image":"tibi.png","description":"体が小さいことを表すクラス。 小器用な立ち回りを活かしたアビリティを習得できる。 ビッグと同時に選ぶことはできない。"},{"name":"オトナ","image":"otona.png","description":"オトナの立ち位置であることを表すクラス。 経験に裏打ちされたアビリティを習得できる。 ２５歳以上でなければ取得できない。"},{"name":"ニューエイジ","image":"new.png","description":"１０年前の災害により、変異を起こしたことを表すクラス。 超能力のアビリティを習得できる。 １５歳以下でなければ取得できない。"},{"name":"キズモノ","image":"kizu.png","description":"消えない傷を受けてしまったことを表すクラス。 その不利を補い生きていくためのアビリティを習得できる。 このクラスを選択した場合、部位ダメージを1受けている状態でスタートする。 この、部位ダメージの入っている「身体部位」を＜キズ＞と呼ぶ。 ＜キズ＞はいかなる手段でも回復しない。"},{"name":"センシ","image":"sensi.png","description":"戦闘が得意なことを表すクラス。 戦闘に必要なアビリティを習得できる。"},{"name":"シノビ","image":"sukauto.png","description":"偵察・調査が得意なことを表すクラス。 探索を有利にするアビリティを習得できる。"},{"name":"ハンター","image":"hunter.png","description":"狩りが得意なことを表すクラス。 飛び道具や罠を用いたアビリティを習得できる。"},{"name":"ハカセ","image":"hakase.png","description":"物知りであることを表すクラス。 知識を活かしたアビリティを習得できる。"},{"name":"ショクニン","image":"syokunin.png","description":"手先が器用であることを表すクラス。 モノづくりに関するアビリティを習得できる。"},{"name":"ホープ","image":"kibou.png","description":"皆の希望である表すクラス。 希望を持つことで運命を変えるアビリティを習得できる。"},{"name":"ママ","image":"mama.png","description":"おかんな立ち位置を表すクラス。 周囲に活力を与えるアビリティを習得できる。"}]
```
## pugで使用する例

```pug:htdocs/rulebook.pug
extends includes/common/_layout
block title
  include includes/common/variables
  include includes/rulebook/variables
  title= title

block headjs
  include includes/common/chart
block body
  body.nohero
    header
      include includes/common/header
    main#main
      section.content
        .container
          h1 終末旅行TRPG 基本ルールブック
          include includes/rulebook/first
          h2 キャラクター
          p あなたの分身となる旅人の作り方
          h2 クラス
          .listB
            .container
              each obj in classList
                article
                  a(href="#")
                    .image(style="background-image: url('../assets/images/" + obj.image + "');")
                    .text
                      h2= obj.name
                      p= obj.description
    include includes/common/footer
```

## 参考
[[Excel] Excel で JSON データを読み込む][*4]
[Node.jsでエクセルファイルを読み込む][*7]
[【JavaScript】Excelで読み込んだデータを文字化けさせずにCSVで書き出す][*1]
[Node.jsでmerged cellしたり色塗ったりしたExcelを出力する][*2]

[*1]:https://qiita.com/shymst/items/42957e8462752b65d875
[*2]:https://qiita.com/maruware/items/c102bbf91bfc10ebe92a
[*4]:https://road2cloudoffice.blogspot.jp/2017/06/excel-excel-json.html
[*7]:https://qiita.com/kai-punipuni/items/a7efe0e65ccd57e0c2f2