Google Apps Script のテンプレートで HTML ファイルを生成する

# はじめに
[毎週行っている作業](https://qiita.com/h-yuki/items/4ba27e415a740d10b870)を自動化する Google Apps Script の、[基本設計](https://qiita.com/h-yuki/items/e00b8743968606582f99)に示した3つ目のステップ「作業結果を埋め込むHTMLの雛型ファイルを作成して、Google ドライブに保存する」について説明します。

今回は(も?)かなりつまらない記事になる予定だったのですが、書く過程で調べていたらいいもの見つけてしまって、ちょっといい感じになりました。その分、長くなってしまいましたが。。。

# 関数仕様
2つの関数を作ります。

## 関数名: createHtmlTemplate
- 引数
    - folder : ファイルを保存する [Folder](https://developers.google.com/apps-script/reference/drive/folder)
    - contents オブジェクト
        - contents.date : 作業対象データの発行日(YYMMDD形式の文字列)
        - contents.items : 作業対象データの配列
            - 配列要素.title : データのタイトル
            - 配列要素.content : データのコンテンツ
- 返り値： [File](https://developers.google.com/apps-script/reference/drive/file)
- 振る舞い
    - contents オブジェクトから HTML を生成する(generateHtmlを呼ぶ)
    - folder の子に「雛型_*YYMMDD*.html」という名前のファイルを作成する
    - ファイルを他のユーザからも参照可能にする

## 関数名: generateHtml
- 引数: contents オブジェクト
- 返り値： String
- 振る舞い
    - contents オブジェクトから HTML を生成する
        - タイトル(title)、見出し1(h1)は発行日(contents.date)
        - 見出し2(h2)は各データ項目のタイトル(contents.items[_n_].title)
        - パラグラフ(p)の中に各データ項目のコンテンツ(contents.items[_n_].content)
        - 実際の作業では、もっと凝ったヘッダー、フッター、まとめ役の方に結果を埋め込んで頂くためのタグなどを含んだ雛型を出力していますが、この記事の内容とは関係しませんので、上記のサンプル仕様で説明します

# テストコード

```javascript:test.gs
function contentsExample() {
  return {
    date: '20190210',
    items: [
      { title: '徒然草', contents: 'つれづれなるままに、日暮らし硯に向かひて、心にうつりゆくよしなしごとを、そこはかとなく書きつくれば、怪しうこそ物狂ほしけれ。' },
      { title: '枕草子', contents: '春は、あけぼの。やうやう白くなりゆく山ぎは、少し明りて、紫だちたる雲の細くたなびきたる。' },
      { title: '方丈記', contents: '行く川の流れは絶えずして、しかも本の水にあらず。よどみに浮ぶうたかたは、かつ消えかつ結びて、久しくとどまりたるためしなし。' }
    ]
  };
}

function testCreateHtmlTemplate() {
  //ルートフォルダに HTML ファイルを生成する
  const folder = DriveApp.getRootFolder();
  const file = createHtmlTemplate(folder, contentsExample());
  Logger.log(file.getUrl());
}

function testGenerateHtml() {
  Logger.log(generateHtml(contentsExample()));
}
```

testCreateHtmlTemplate では、実行後に Google ドライブでルートフォルダ(マイドライブ)に HTML ファイルができていることを確認して下さい。testGenerateHtml では、Ctrl+Enter キーでログを表示して確認して下さい。期待する正解 HTML については省略します。

# 関数コード
## createHtmlTemplate()

こちらは簡単です。testCreateHtmlTemplate を実行できるようにするために、generateHtml も仮実装します。

```javascript:main.gs
function createHtmlTemplate(folder, contents) {
  Logger.log('作業結果を埋め込むHTMLの雛型ファイルを作成して、Google ドライブに保存する');
  const file = folder.createFile('雛型_' + contents.date + '.html', generateHtml(contents), MimeType.HTML);
  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  return file;
}

function generateHtml(contents) {
  return '<h1>Hello, GAS World.</h1>';
}
```

[File#setSharing()](https://developers.google.com/apps-script/reference/drive/file#setSharing(Access,Permission))が、ファイルの共有設定をするコードです。上記では、リンクを知っている人なら誰でも閲覧可という設定にしています。確認方法としては、Google ドライブでファイルの詳細を表示して、クリップのような形の「共有」アイコンにマウスを重ねた時に、「リンクを知っている全員が閲覧できます」と表示されていればOKです。また、テストコードでログにファイルの URL を出力していますので、これをコピーして、ブラウザのシークレットウィンドウを開いて URL をペイストし、ファイルにアクセスできればOKです。ログアウトして確認してもよいですが、シークレットウィンドウ使うと便利だと思います。

## generateHtml()
いよいよこの記事のメイントピックスである HTML 生成です。

### テンプレートリテラル
Google Apps Script に移行する前は、普通の(?) JavaScript で作った HTML 生成プログラムを使っていました。そこでは、テンプレートリテラルという便利な構文を使っていました。簡単に言うと、バッククォート(``` ` ```)で囲まれたテキストを、改行も含めそのまま文字列として扱え、また```${ }```の中に書いた JavaScript は評価して置き換えてくれるものです。いわゆるヒアドキュメントのように文字列を作ることができます。以下のコードは、テンプレートリテラルを使って generateHtml() を実装した例です。実行結果が見られるように、HTMLファイルとして作成しています。

```html:generateHtml.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>HTML生成</title>
<script type="text/javascript"><!--
function contentsExample() {
  return {
    date: '20190210',
    items: [
      { title: '徒然草', contents: 'つれづれなるままに、日暮らし硯に向かひて、心にうつりゆくよしなしごとを、そこはかとなく書きつくれば、怪しうこそ物狂ほしけれ。' },
      { title: '枕草子', contents: '春は、あけぼの。やうやう白くなりゆく山ぎは、少し明りて、紫だちたる雲の細くたなびきたる。' },
      { title: '方丈記', contents: '行く川の流れは絶えずして、しかも本の水にあらず。よどみに浮ぶうたかたは、かつ消えかつ結びて、久しくとどまりたるためしなし。' }
    ]
  };
}

function generateHtml(contents) {
  var html =
`<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>${contents.date}</title>
  </head>
  <body>
    <h1>${contents.date}</h1>
    <!-- Contents -->
`;
  for(var i=0; i<contents.items.length; i++) {
    html +=
`    <h2>${contents.items[i].title}</h2>
    <p>
      ${contents.items[i].contents}
    </p>

`;
  }
  html +=
`  </body>
</html>
`;
  return html;
}
// --></script>
</head>

<body onLoad='console.log(generateHtml(contentsExample()))'>
<h1>ログを見て下さい</h1>
</body>
</html>
```

ブラウザ(Chrome)で開いて、コンソールを表示させるとこんな風に HTML が生成できていることがわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/354068/24312f3e-b116-d146-a45c-b46568a8f251.png)

### Google Apps Script ではテンプレートリテラルが使えない
それではと、main.gs の generateHtml() を上記 HTML 内の記述に置き換えて保存しようとすると、バッククォート(``` ` ```)のところで「文字が無効です。」となり、保存すらできません。残念ですが、コードは次のように書き換えるしかありません。これが最初につまらないと書いたコードです。えっ、大して変わらない?

```javascript:main.gs
function generateHtml(contents) {
  var html =
'<!DOCTYPE html>\n' +
'<html lang="ja">\n' +
'  <head>\n' +
'    <meta charset="UTF-8" />\n' +
'    <title>' + contents.date + '</title>\n' +
'  </head>\n' +
'  <body>\n' +
'    <h1>' + contents.date + '</h1>\n' +
'    <!-- Contents -->\n';
  
  for(var i=0; i<contents.items.length; i++) {
    html +=
'    <h2>' + contents.items[i].title + '</h2>\n' +
'    <p>\n' +
'      ' + contents.items[i].contents + '\n' +
'    </p>\n' +
'\n';
  }
  
  html +=
'  </body>\n' +
'</html>\n';
  return html;
}
```

### HTML テンプレートが使えることを発見!
この記事を書くにあたって、改めてドキュメントを調べていたら、HTML テンプレート([Templated HTML](https://developers.google.com/apps-script/guides/html/templates))が使えることがわかりました。Google Apps Script では Web アプリケーションを開発することができますので、当たり前と言えば当たり前かも知れませんが、JSP 風のテンプレートをサポートしていました。テンプレートは、スクリプトエディタのメニューから「ファイル」→「新規作成」→「HTML」と選択して作成します。

```html:template.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title><?= contents.date ?></title>
  </head>
  <body>
    <h1><?= contents.date ?></h1>
    <!-- Contents -->
<? for(var i=0; i<contents.items.length; i++) { ?>
    <h2><?= contents.items[i].title ?></h2>
    <p>
      <?= contents.items[i].contents ?>
    </p>

<? } ?>
  </body>
</html>
```

値を返す式は```<?= expression ?>```のように書きます。繰り返しや条件分岐などは、```<? statement ?>```のように書きます。簡単ですね。

generateHtml() 関数は以下のようになります。[HtmlService#createTemplateFromFile()](https://developers.google.com/apps-script/reference/html/html-service#createTemplateFromFile(String)) で生成した [HtmlTemplate](https://developers.google.com/apps-script/reference/html/html-template) オブジェクトに対して属性を定義することで、テンプレートに対して値を渡す(テンプレート内から参照する)ことができます。

```javascript:main.gs
function generateHtml(contents) {
  const t = HtmlService.createTemplateFromFile('template');
  t.contents = contents;
  return t.evaluate().getContent();
}
```

### HTML コメントが消える!?
いやぁ～、すっきりしましたね。生成された HTML ファイルを、当初のテンプレートリテラルで生成していたものと比較してみましょう。あれ? 一致しません。よく調べると、HTML コメント ```<!-- Contents -->``` が出力されていません。HTML としてはもちろん等価なのですが、実際の作業では後段の処理でコメント行を元に文字列処理を行っていて、コメント行に消えてもらうと困るのです。[現在使っているツール](https://qiita.com/h-yuki/items/4ba27e415a740d10b870#%E7%8F%BE%E5%9C%A8%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%83%84%E3%83%BC%E3%83%AB)の最後の「最終的に掲載可能な形式には、シェルスクリプトで変換しています」のところです。もちろん、そちらも改善すればよいのですが、インターフェイスを保ったままコードを入れ替えていくのはリファクタリングの基本ですから、まずはここで同じ HTML を出力するようにしておきたいです。

いろいろ調べてみましたが、標準 API では実現できそうにありません。私の理解では、HtmlTemplate はこの記述を保持しているのですが、[HtmlTemplate#evaluate()](https://developers.google.com/apps-script/reference/html/html-template#evaluate()) で [HtmlOutput](https://developers.google.com/apps-script/reference/html/html-output) オブジェクトに変換した時か、[HtmlOutput#getContent()](https://developers.google.com/apps-script/reference/html/html-output#getContent()) で文字列に変換する際に、コメントは削除されます。

苦肉の策として、以下の方法を考え付きました。当然誤変換の元になり得るので、コンテンツに制約を課してしまいますが、今回のプロジェクトではこれを採用しました。

まず、HTML テンプレートのコメント行は、以下のように先頭の```<```を削除します。

```html:template.html
    <h1><?= contents.date ?></h1>
    !-- Contents -->
<? for(var i=0; i<contents.items.length; i++) { ?>
    <h2><?= contents.items[i].title ?></h2>
```

これでコメント行は単なる文字として出力されますので、その後に```!--```を```<!--```に変換してやればよいのです。generateHtml() 関数は以下のようになります。

```javascript:main.gs
function generateHtml(contents) {
  const t = HtmlService.createTemplateFromFile('template');
  t.contents = contents;
  return t.evaluate().getContent().replace(/!--/g, '<!--');
}
```

# おわりに
今回は、Google ドライブに共有ファイルを作る方法と、HTML テンプレートの使い方を紹介しました。長かったですね ^_^;