◆ テンプレートリテラルでテンプレート埋め込み処理をするとファイルは JavaScript ファイルになる
  ◆ HTML を作る場合は HTML の前後に JavaScript 処理が入って邪魔
  ◆ エディタのサポートも効かずに不便
◆ HTML だけを書いたファイルを使ってテンプレートリテラルの埋め込み処理をする
◆ 事前ビルドは必要

以前も書いた JavaScript のテンプレート問題です

ここまでのまとめ

簡単にまとめると JavaScript には テンプレートリテラル機能があるので単純埋め込みのテンプレートエンジンはいらなくなったはずなのに JavaScript ファイルとして書かないといけないせいで テンプレートファイルとして使えなくて扱いづらい のが問題です
テンプレートを モジュールファイルに分けるとこういう感じです

const html = require("./html-helper.js")
module.exports = values => html`
<p>このテンプレートリテラルの中に HTML を書く</p>
`

HTML だけでは済まず 上下に JavaScript としてのコードが少し含まれてしまいます
また 全体として JavaScript ファイルとなるのでエディタサポートが使えません

HTML ファイルを変換するツールを作った

この問題のせいでやっぱり不便すぎたので どうにかしたくて変換するツールを作りました
HTML ファイルとして書きたいので 上の例だと

<p>このテンプレートリテラルの中に HTML を書く</p>

だけを書いて

const html = require("./html-helper.js")
module.exports = values => html`



`

のような部分は自動で補います

また require やタグ関数の html も書かなくて済むようにしました
JavaScript の中の Identifier に対して自動で require を追加します
タグ関数はファイルの拡張子を使うことにしました

変換ツール


使いかた

上の URL にもある例を使います

フォルダ

この変換ツールは src, dst, helper の 3 つのフォルダが必要です

node index.js --dst /path/to/templates

のようにコマンドライン引数で指定できます
指定がない場合はコマンドを実行したフォルダ内の src, dst, helper フォルダになります

src にはテンプレートファイルを入れます
今回は HTML ファイルなので .html 拡張子ですが SQL ファイルを作るなら .sql 拡張子です
拡張子はテンプレートリテラルのタグ関数の名前になります

dst は出力先のフォルダです
すべて .js ファイルで出力されます

helper にはヘルパーとして使う .js ファイルを入れます
自動 require されるものです
HTML 中で foo という関数を使うなら foo.js を helper に用意します
自動 require では foo.js の module.exports の値がテンプレートファイル中の foo として使えます
タグ関数も helper から自動で require するものなので .html ファイルを使うには html.js が必要です

html タグ関数

const htmlize = (value) => {
if (Array.isArray(value)) return value.map(htmlize).join("")
if (value.html) return value.html
if (value.text) return htmlEscape(value.text)
return htmlEscape(String(value))
}

module.exports = (tpls, ...values) => {
return tpls.reduce((a, b, i) => a + htmlize(values[i - 1]) + b)
}

htmlEscape 関数は略してますが html.js はこういう感じです
タグ関数として使うので エクスポートする関数の引数はタグ関数用のものです

<div>${"<br>"}</div>

と書くと自動でエスケープされ

<div>${{ html: "<br>"}}</div>

と書くとエスケープされずそのまま埋め込まれます
配列が渡されると 中身それぞれに対して 上のエスケープするしないを処理してすべてを埋め込みます

この処理は変換とは関係ない 例として使ってる html.js の中身なので自由に変更できます
この記事中の HTML テンプレートはこのタグ関数を使う前提で書いています

出力されるファイル

変換後に出力されるテンプレートのエクスポートされる部分はこういう感じになります

module.exports = $ => ({ html: html`<div>ここはそのまま</div>` })

html`` の内側に .html ファイルの中身がそのまま入ります
引数は $ として受け取ります

console.log(require("./templates/page.js")({ title: "Test Page" }))

のような引数なら

[page.html]
<h1>${$.title}</h1>

のようなテンプレートにすると "Test Page" を h1 の中に埋め込めます

同じフォルダの require

helper フォルダにマッチする .js ファイルがない場合は 同じフォルダからの require になります
出力先の dst に設定したフォルダです
この機能のおかげでテンプレート内で他のテンプレートを使えます

[header.html]
<header>${$}</header>

[page.html]
<div>${header("TITLE")}</div>

page.html 中の header は header.html を変換した .js ファイルがエクスポートするものです
引数として "TITLE" を渡しているので header.html 内の $ は "TITLE" となっています

各フォルダの中はファイルのみ

自動 require でモジュールを探す都合上 src, helper フォルダの中のフォルダは無視されます
src, helper フォルダの中に直接ファイルを配置する必要があります

例のリポジトリのテンプレートを見ていきます
src フォルダは 3 つのファイルがあります

[page1.html]
<div>
<div>1 + 1 = ${1 + 1}</div>
<div>TEXT: ${$.html}</div>
<div>HTML: ${{ html: $.html }}</div>
<div>${h1("H1 TEXT")}</div>
<div>${list($.items)}</div>
</div>

[list.html]
<ul>${$.map(item)}</ul>

[item.html]
<li>${$.name}</li>

h1 は helper で h1.js はこうなっています

[h1.js]
const html = require("./html.js")

module.exports = (text) => {
return { html: html`<h1>${text}</h1>` }
}

リポジトリでは package.json の dependencies に変換ツールを追加しているので

yarn install

で変換ツールがインストールされます
コマンドラインツールとして動くようにしてるので

yarn tplb

と実行すると変換処理が実行されます

yarn tplb --src /path/to/src --dst /path/to/dst --helper /path/to/helper

でフォルダ指定できます
作りが雑なので --src=/path/to/src のような = を使う形式は非対応です
スペースが必要です

実行すると dst フォルダに page1.js と list.js と item.js ができています
リポジトリでは変換後のものを dst フォルダに含めているので変換しなくても結果を直接見れます

また index.js は変換後の .js ファイルを使う例です

const page1 = require("./dst/page1.js")

const { html } = page1({
html: `bold : <b>BOLD</b>`,
items: [
{ name: "ITEM1" },
{ name: "ITEM2" },
{ name: "ITEM3" },
]
})

console.log(html)

を実行した結果の HTML はこうなります
output.html として含めています

<div>
<div>1 + 1 = 2</div>
<div>TEXT: bold : &lt;b&gt;BOLD&lt;/b&gt;</div>
<div>HTML: bold : <b>BOLD</b></div>
<div><h1>H1 TEXT</h1></div>
<div><ul><li>ITEM1</li><li>ITEM2</li><li>ITEM3</li></ul></div>
</div>

問題点

これだけ見ると一見良さそうな気がしなくもないですが 思いつきでサッと作ったものだけあって 制限も多いです
ここまで書いたものに加えて 自動 require と エディタの ${} の扱いの問題があります

自動 require

acorn でソースコードをパースして type が Identifier の Node の name を require 対象としています
${} の中の処理が複雑になって変数を使ったり コールバック関数を定義したりすると 必要ないものまで require しようとしてエラーが出るはずです
スコープ上に存在しない変数の使用のみに制限すればいいのですが 簡単にできなさそうだったので対応してません
eslint で警告出たりするので できなくはないはずですけど

エディタと ${}

HTML ファイルとしてしまったので エディタ上では HTML ファイルとして認識されます
これによって色や補完などのサポート機能が使えます

しかし HTML などのテンプレートファイルで ${} を特別視しないため ${} の中で "</div>" などを使うと HTML タグとしてみなされます
タグの対応が合わずエラー扱いされたり 自動フォーマットでおかしくなったりと言った問題が出るはずです

見方を変えると良いのかも?

不便な点ではありますが これらはテンプレート中の ${} で複雑なことをしようとするから発生する問題です
テンプレート中の処理は減らしできるだけシンプルに書くべきだと思いますし 複雑になるなら helper 側でやるほうが良いと思います
require したいものと引数の $ 以外に Identifier を書けない制約があればテンプレートが複雑になることはほぼないと思います

いる?

で 結局作ってみたけどこれいるの?ということですが……結局使わなそうな気はしてます

一番の理由はテンプレートの変換の手間です
変更のたびに変換も必要って面倒です
Webpack などをあまり使いたくないのと一緒です

特にこれは JavaScript 標準のテンプレートリテラルで書けるから余計な手間がいらなくて高速なのが重要なところです
その魅力がなくなるならあえてこれを使うメリットはほぼないのですよね

ejs などの埋め込み系のテンプレートエンジンでも コンパイルしたときに これと同じようなテンプレートリテラル機能を使った JavaScript として出力することもできますし
それなら記法に自由度のあるテンプレートエンジンの方がメリットが大きいです
ejs では

<%= value %>
<%- value %>

と書けばエスケープの有無を変更できます
テンプレートリテラルだと ${} でしか書けないので そこに何を入れるかで判断するしかないです

${ value }
${ { html: value } }

他にも出力せずに JavaScript 処理だけをしたり 分岐や繰り返しなどもテンプレートエンジンのほうが便利です
JavaScript そのものだけで書けるメリットがあるから それらは多少不便だけど許容範囲だったのですが 一手間書けるならテンプレートエンジンでいい気がしてきます