この記事は次の記事の続きです。
前編では、JavaScritのタグ付きテンプレートをつかって、自動的にHTMLエスケープをするテンプレートエンジン 3号を作りましたた。
課題
次のような課題が残っています。
- 「埋め込み式」がHTML文字列を返すときに、テンプレートエンジン 3号を使うと、エスケープしなくてよいHTML文字列をエスケープする
- 特に、部分テンプレートを使う時に、HTML文字列が返ってくるのかエスケープしていない値が返ってくるのか、判別がややこしい
テンプレートエンジン 3号に渡す文字列が、エスケープが必要かどうかの情報を付加したいです。
に何かしらのフラグを持たせたいです。
案1 エスケープ済みフラグ
例えば テンプレートエンジン で生成する文字列にエスケープ済みのフラグを付けてはどうでしょうか?
つぎのように、テンプレートエンジンが返す文字列にプロパティを追加します。
import escape from 'lodash.escape'
function autoEscape(strings) {
const [first, ...rest] = strings
let out = first
for (const value of [...arguments].slice(1)) {
if (value.isAlreadyEscaped) {
// すでにエスケープ済みのとき
out = `${out}${value}${rest.shift()}`
} else {
out = `${out}${escape(value)}${rest.shift()}`
}
}
out.isAlreaddyEscaped = true
return out
}
残念ながら、これは上手く動きません。
JavaScriptではプリミティブ型にプロパティを追加できません。
文字列はプリミティブ型なのでプロパティを追加できません。
次のように、Stringクラスのインスタンスにするとプロパティを追加できます。
import escape from 'lodash.escape'
function autoEscape(strings) {
const [first, ...rest] = strings
let out = first
for (const value of [...arguments].slice(1)) {
if (value.isAlreadyEscaped) {
out = `${out}${value}${rest.shift()}`
} else {
out = `${out}${escape(value)}${rest.shift()}`
}
}
out = new String(out) // Stringのインスタンス化
out.isAlreaddyEscaped = true
return out
}
まあ、でもテンプレートエンジンの出力した文字列に、テンプレートエンジン独自のフラグが入っているのはかっこ悪いですよね。
JavaScript標準の文字列が返るべきですよ。
案2 アロー関数式
案1では、テンプレートエンジンの出力にフラグを立てました。
では、テンプレートエンジンの入力にフラグを立てられないでしょうか?
次のイメージです。
autoEscape`
<ul>
${ new EscapedString(items.map(item => `<li>${item}</li>`)) }
</ul>
`
これならテンプレートエンジン内で、型を判定して分岐出来そうです。
しかし、new EscapedString()
で、かこうのはちょっと面倒ですし、かっこよくないです。
できれば、頭にマークを付けるくらいで、フラグを立てられないでしょうか?
できます。
まず、テンプレートリテラルに渡される$hoge
は埋め込み式です。
そう式なんです。
一方、JavaScriptにはアロー関数式と呼ばれる式があります。
代表的な記法が
() => 式
です。
そう、式の頭に() =>
がついた式です。
これを使えば、頭にマークを付けるくらいで、フラグを立てられます。
次の書き方を想定しています。
autoEscape`
<ul>
${ () => items.map(item => `<li>${item}</li>`) }
</ul>
`
new EscapedString()
で、かこうのにくらべてシンプルになりました。
アロー関数式は式です。そのまま埋め込み式として使えます。
また、アロー関数式は値ではなく関数を返します。
テンプレートエンジンは、埋め込み式で渡された値が「関数であればエスケープしない」と判断できます。
import escape from 'lodash.escape'
function autoEscape(strings) {
const [first, ...rest] = strings
let out = first
for (const value of [...arguments].slice(1)) {
if (typeof value === 'function') {
// 関数のときは、エスケープしない。関数を実行して値を取り出す。
out = `${out}${ value() }${rest.shift()}`
} else {
out = `${out}${escape(value)}${rest.shift()}`
}
}
return out
}
テンプレートエンジン 4号の完成です。
使用例
HTML文字列を埋め込む
function renderHTML(name, email, selfIntroduction) {
return autoEscape`
<div class="profile">
<div class="name">${name}</div>
<div class="e-mail">${email}</div>
<div class="badge">${() => badge}</div>
</div>
`
}
element.innerHTML = renderHTML('ledsun', 'ledsun@example.com', '<img src="..."></a>')
埋め込み式として渡す、値が安全なHTML文字列だと知っている場合は、冒頭() =>
をつけると二重エスケープが回避できます。
部分テンプレート
部分テンプレートを使う場合も、次のように書けます。
// 部分テンプレート
function renderList(item) {
return () => autoEscape`<li>${(item)}</li>`
}
部分テンプレートは自身がエスケープ済みの文字列を返すか知っています。
エスケープ済みの文字列を返すときは関数を返します。
function renderHTML(items) {
return autoEscape`
<ul>
${ items.map(renderList) }
</ul>
`
}
element.innerHTML = renderHTML(['リンゴ', 'みかん', 'なし'])
部分テンプレートを埋め込むテンプレートは、埋め込まれているものがエスケープ済みの文字列を返すかどうか気にせず埋め込むことが出来ます。
まとめ
() =>
つけるの、かっこいいでしょう?