※スマホ対応はしてません。

タグ: テンプレート記法

テンプレートを自作しよう。(現代的JavaScriptおれおれアドベントカレンダー2017 – 13日目)

カテゴリー: JavaScript

現代的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.length は values.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ってなんじゃ……。

参考

テンプレート記法で簡単文字列組み立て。(現代的JavaScriptおれおれアドベントカレンダー2017 – 12日目)

カテゴリー: JavaScript

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

概要

back-tick (back-quote) ` … ` で括ると「テンプレートリテラル」となり、変数を埋め込んだりできます。

const user = { name: 'Alice', birthYear: 2000 };
const message = `${user.name} is ${new Date().getFullYear() - user.birthYear}-year-old.`;

他に改行したり、独自のテンプレート関数を適用させることも。

テンプレートリテラル

今まで動的な情報を使って文字列を組み立てる場合、 + で接続してあれこれしてきました。

console.log('timestamp: ' + Date.now() + ' at some place.');

テンプレートリテラルの場合、 ${ … } という書き方を使って文字列中に埋め込むことで、より読みやすく記述することができます。

console.log(`timestamp: ${Date.now()} at some place.`);

うは、Syntax highlightingが間に合ってないすね。

式

${ … } の中身には式を書くことができます。

console.log(`result: ${1 + 2}`);

+ 連結時と同様、 .toString() が呼び出され文字列になります。

式は書けるけど文は書けません。 for とかは駄目。

改行

テンプレートリテラル中では改行もそのまま使えます。

console.log(`Hello
             Beautiful
             World!!`);
Hello
             Beautiful
             World!!

インデントはアレなんだけど。

今まで通り \n での改行もできます。

生文字列

back tickの前に「タグ (tag) 」を置くことで、特別なテンプレート化を行うことができます。

String.raw() というのが用意されていて、これを使うと文字列を「そのまま」の状態で扱えます。

console.log("Ginpei \"Sushi-guy\" Takanashi");  // Ginpei "Sushi-guy" Takanashi
console.log(`Ginpei \"Sushi-guy\" Takanashi`);  // Ginpei "Sushi-guy" Takanashi
console.log(String.raw`Ginpei \"Sushi-guy\" Takanashi`);  // Ginpei \"Sushi-guy\" Takanashi

back slash \ でエスケープできるはずが、されなくなります。もちろん今まで通りの \n でも改行できなくなってしまいました。

改行や ${ … } による処理埋め込みは可能です。そして、当たり前だけど、そっちで普通の文字列にしたらエスケープされます。

console.log(String.raw`\'
${'\''}`);
\'
'

無駄にややこしくしちゃったかも。ごめんね。

普通の関数として使ってはいけない

これは駄目。

console.log(String.raw(`\n`));  // Exception: TypeError: can't convert undefined to object

この↑場合だと、先にテンプレートリテラルを普通に解釈して、その結果の文字列を与えることになります。

括弧なしで後ろに付けてください。(空白はあってもなくてもよろしい。)

console.log(String.raw `\n`);  // \n

自分でテンプレート関数を作るようになったらよくわかるかも。

タグ関数

String.raw() みたいなものをタグ関数 (tag function) 、タグ関数を伴うテンプレートリテラルの仕組みをタグ付きテンプレート (tagged template) と呼ぶようです。

加熱調理済み紐

ちなみにエスケープしない生の文字列のことを “raw strings” 、そうでないエスケープされるものを “cooked strings” と呼ぶみたいです。

独自テンプレート

前項 String.raw() は、使い方は特殊だけど結局は関数です。同じような関数を作成することで、独自のテンプレート処理を行うことができます。

別稿参照。

その他

この記号 ` の名前

MDN (en)だと “back-tick” と呼んでいる。

Wikipediaには “grave accent” の名前で項目があり、プログラミング界隈での別名として “backquote” 、 “backtick” が紹介されている。

Programmers use the grave accent symbol as a separate character (i.e., not combined with any letter) for a number of tasks. In this role, it is known as a backquote or backtick.

テンプレートリテラル vs テンプレート文字列

仕様的には “template literals” 。

MDNによると過去に「テンプレート文字列」と呼ばれていたみたいです。

ES2015 / ES6 仕様の以前のエディションでは、”template strings” と呼ばれていました。

参考