現代的JavaScriptおれおれアドベントカレンダー2017 – 13日目

概要

昨日のやつでJavaScriptのテンプレート記法をご紹介したわけなんだけども、そのテンプレート記法を使った処理を「タグ関数」を作って独自に拡張することができます。

// 連結するだけの(非現実的な)タグ関数
function tagger(template, ...substitutions) {
  console.log(template[0]);  // => "hoge "
  console.log(template[1]);  // => " fuga "
  console.log(template[2]);  // => " piyo"
  console.log(substitutions[0]);  // => 10
  console.log(substitutions[1]);  // => 25

  return template[0] + substitutions[0] + template[1] + substitutions[1] + template[2];
}

const n = 10;
console.log(      `hoge ${n} fuga ${20 + 5} piyo`);  // => "hoge 10 fuga 25 piyo"
console.log(tagger`hoge ${n} fuga ${20 + 5} piyo`);  // => "hoge 10 fuga 25 piyo"

...substitutions は可変長引数的なやつです。別稿参照。

タグ関数

テンプレート記法を分解した結果を受け取り、処理して返す関数です。

デフォルトで String.raw() というものがあります。

console.log(String.raw`foo\nbar\n${"boo"}`);  // => "foo\\nbar\\nboo"

この関数を自作することができます。

例えば普通に繋げるだけの「何もしない」タグ関数を作る場合、こんな感じになります。

function plain(strings, ...values) {
  // strings[0] === "hoge "
  // values[0] === 10
  // strings[1] === " fuga "
  // values[1] === 25
  // strings[2] === " piyo"

  // 文字列を連結してゆく変数
  // (初期値として先頭のもの "hoge " を与えておく。)
  let result = strings[0];

  // 先頭は既に追加済みなので飛ばし、
  // 残りを順次追加
  for (var i = 1; i < strings.length; i++) {
    result += values[i - 1] + strings[i];
  }

  return result;
}

const n = 10;
console.log(     `hoge ${n} fuga ${20 + 5} piyo`);  // => "hoge 10 fuga 25 piyo"
console.log(plain`hoge ${n} fuga ${20 + 5} piyo`);  // => "hoge 10 fuga 25 piyo"

values には ${} の部分が、 strings にはそうではない普通の部分がそれぞれ配列で格納されます。

strings.lengthvalues.length + 1 に必ずなります。なるはず。

例えば `A${99}Z` の場合、 strings["A", "Z"]values[99] となります。

普通の文字列部分がない場合

文字がない場所は空文字列になります。

`${99}` の場合、 ${} の前後に空文字列があるとみなされます。つまり strings["", ""]values[99] となります。

同様に `${-1}${99}` の場合、 strings["", "", ""]values[-1, 99] となります。

一行で普通に繋げるやつ(おまけ)

こんな感じでどっすかね。

// 普通に結合
function plain(strings, ...values) {
  return strings.slice(1).reduce((result, s, i) => result + values[i] + s, strings[0]);
}

console.log(plain`hoge ${1} fuga ${2} piyo`)

Array#reduce()らくちんちょうべんり。(読みやすいかは別だけど。)

与えられた数値の小数点以下を揃える

${} には数値を与えられる前提で、各値の小数点以下二桁まで表示する、というやつをやってみたいと思います。

// decimal-regularized text
function drt(strings, ...values) {
  let result = '';

  for (let i = 0; i < strings.length; i++) {
    result += strings[i];

    const value = values[i];
    if (typeof value === 'number') {
      const sValue = Math.floor(value * 100).toString();  // 3.1415 -> 314.15 -> 314 -> "314"
      const srValue = `${sValue.slice(0, -2)}.${sValue.slice(-2)}`;  // "314" -> "3" + "." + "14"
      result += srValue;
    }
  }

  return result;
}

console.log(drt`πは${Math.PI}です。`);  // => "πは3.14です。"
console.log(drt`私の身長は${167.3}cmです。`);  // => "私の身長は167.30cmです。"
console.log(drt`私の体重は${66}kgです。`);  // => "私の体重は66.00kgです。"

タグ関数を返す関数

前項の「与えられた数値の小数点以下を揃える」やつ、何桁で揃えたいかってあるじゃないですか。そこを引数で受け取れるようにすると便利かもしれません。

というわけで、タグ関数を(クロージャを使って生成して)返す関数を用意してみました。

// decimal-regularized text
function drt(digits) {
  // この中身は、digitsを使ってる以外はさっきのと一緒
  return function(strings, ...values) {
    let result = '';

    for (let i = 0; i < strings.length; i++) {
      result += strings[i];

      const value = values[i];
      if (typeof value === 'number') {
        const sValue = Math.floor(value * 10 ** digits).toString();  // 3.1415 -> 314.15 -> 314 -> "314"
        const srValue = `${sValue.slice(0, -digits)}.${sValue.slice(-digits)}`;  // "314" -> "3" + "." + "14"
        result += srValue;
      }
    }

    return result;
  }
}

const drt1 = drt(1);
console.log(drt1`πは${Math.PI}です。`);  // => "πは3.1です。"

const drt5 = drt(5);
console.log(drt5`πは${Math.PI}です。`);  // => "πは3.14159です。"

文字列以外を返す

例えばHTML文字列を受け取ったら、それからDOMツリーを構築して返す、なんてのもありだと思います。

function html(strings, ...values) {
  // 普通に結合
  const html = strings[0] + strings.reduce((result, s, i) => values[i-1] + s, '');

  // DOMツリー生成
  // (サボってjQuery使います。)
  const tree = jQuery(html)[0];

  return tree;
}

const title = 'Hello World!';
document.body.appendChild(html`
  <div class="main">
    <h1>${title}</h1>
  </div>`)

テンプレート関数

関数を返すタグ関数なんてのもいけます。

さっきのを拡張して、任意のデータを受け取ってDOMツリーを形成するやつにしました。

// データ
const items = [
  { name: 'Sugoi pen', price: '3000' },
  { name: 'Sugokunai pen', price: '130' },
];

// 「DOMを生成する関数」を返すタグ関数
function html(strings, ...keys) {
  // DOMを生成する関数
  return function(data) {
    // 結合
    let html = strings[0];
    for (var i = 1; i < strings.length; i++) {
      const key = keys[i - 1];
      const value = data[key];
      html += value + strings[i];
    }

    // DOMツリー生成
    // (サボってjQuery使います。)
    const tree = jQuery(html)[0];

    return tree;
  };
}

// テンプレート
const itemTemplate = html`
  <li class="sugoi-item">
    ${'name'}
    <span class="price">(${'price'}円)</span>
  </li>
  `;

// 実行
const elList = document.querySelector('ul#yabai-list');
items.forEach(data => {
  elList.appendChild(itemTemplate(data));
});

Mustacheで <h1>{{title}}</h1> と書いていたところを <h1>${'title'}</h1> と書くようにした感じです。

デモ:

普通の文字列を解析してあれこれしてオブジェクト生成して返す

昔作ったのを思い出しました。

Vue.jsでコンポーネントが上位コンポーネントから値を受け取る (props) とき、その値の妥当性を検証する仕組みがあります。こんな感じで諸々指定していく感じです。

const MyComponent = {
  props: {
    simpleString: { type: String },
    requiredNumber: { type: Number, required: true },
    defaultBoolean: { type: Boolean, default: true },
  }
}

これをもうちょっと簡単に書けないかなーと思って作りました。こういうふうに書けます。

const pt = require('vue-props-template')
 
const MyComponent = {
  props: pt`
    string simpleString
    required number requiredNumber
    boolean defaultBoolean = ${true}
  `
}

文字列を元にオブジェクトを返しているわけです。

その後全然触ってないや。年末にまたやろうかな。

うっ頭が

その他

raw

String.raw() を普通に関数呼び出しすると、こんな感じだとエラーになってしまう。

String.raw(['foo', 'bar'], 1);  // Exception: TypeError: can't convert undefined to object

どうも実は第一引数の配列に raw というプロパティが追加されていて、そちらを見ているみたい。ので、それを与えてやれば大丈夫。

String.raw({ raw: ['foo', 'bar'] }, 1);  // => "foo1bar"

この raw はエスケープをものともしないやつらが格納されてます。下記別稿「生文字列」の項を参照。

自作のタグ関数から strings.raw を見るのも可能。使い道はぱっと思いつかないけど、 String.raw() 的なことをしたい場面で便利なはずです。

function rawSomething(strings, ...values) {
  const rawStrings = strings.raw;
  // ...
}

テンプレート記法の解析とオブジェクト生成

“12.2.9.3 Runtime Semantics: GetTemplateObject ( templateLiteral )” がその仕様だと思うんだけど、正直理解し切れてません。

Realmってなんじゃ……。

参考