LoginSignup
11
0

JavaScriptのテンプレートエンジンをつかって簡易テンプレートエンジンをつくる 後編

Last updated at Posted at 2023-12-21

この記事は次の記事の続きです。

前編では、JavaScritのタグ付きテンプレートをつかって、自動的にHTMLエスケープをするテンプレートエンジン 3号を作りましたた。

課題

次のような課題が残っています。

  1. 「埋め込み式」がHTML文字列を返すときに、テンプレートエンジン 3号を使うと、エスケープしなくてよいHTML文字列をエスケープする
  2. 特に、部分テンプレートを使う時に、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(['リンゴ', 'みかん', 'なし'])

部分テンプレートを埋め込むテンプレートは、埋め込まれているものがエスケープ済みの文字列を返すかどうか気にせず埋め込むことが出来ます。

まとめ

() => つけるの、かっこいいでしょう?

参考

11
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
0