Riot.js v4 カスタムテンプレートを実装する

[Riot.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/riotjs) の13日目が空いていたので埋めます。

## はじめに
前回の[Riot.js v4 HTMLのテンプレートエンジンにPugを使う](https://qiita.com/nekijak/items/05a812cc437d064d7813)で、[@riotjs/compiler](https://github.com/riot/compiler)の`registerPreprocessor`で自由にテンプレートを登録できるということがわかりました。

これを検証してみようと思います。

## 動きを確認
まずはデフォルトのテンプレートエンジンの動きがどうなっているかを確認。
https://github.com/riot/compiler/blob/master/src/preprocessors.js

```javascript:/riot/compiler/blob/master/src/preprocessors.js
export const preprocessors = Object.freeze({
  javascript: new Map(),
  css: new Map(),
  template: new Map().set('default', code => ({ code }))
})
```

なるほどなるほど、引数で受け取った`code`を元になんやかんやして、`{ code: "最終的なコード" }`を返せば良さそうですね。
デフォルトでは何もしないこともわかりました。

動きを確認するために、適当に登録してみます。

```javascript:webpack.config.js
const path = require('path');
const compiler = require('@riotjs/compiler');

// カスタムテンプレートを登録
compiler.registerPreprocessor('template', 'step_count', (code, options) => {
  console.log("Hello World!!");
  console.log("code:", code);
  console.log("options:", options);
  return {
    code: code
  };
});

module.exports = {
  mode: 'development',
  //mode: 'production',

  entry: './src/scripts/index.js',
  output: {
    path: path.resolve(__dirname, 'app/scripts'),
    filename: 'bundle.js',
    publicPath: '/scripts/',
  },

  devtool: 'inline',
  //devtool: 'source-map',

  module: {
    rules: [
      {
        test: /\.riot$/,
        exclude: /node_modules/,
        use: [{
          loader: '@riotjs/webpack-loader',
          options: {
            hot: true, // set it to true if you are using hmr
            // add here all the other @riotjs/compiler options riot.js.org/compiler
            template: 'step_count'
          }
        }]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};
```
```json:package.json
{
  "name": "riotv4-custom-template-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --mode production --devtool source-map",
    "start": "webpack-dev-server --inline --watch --hot --colors --content-base app/ --open-page index.html --historyApiFallback true -d --port 4500"
  },
  "keywords": [],
  "author": "KAJIKEN <kentaro@kajiken.jp> (http://kajiken.jp)",
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/preset-env": "^7.7.6",
    "@riotjs/compiler": "^4.5.3",
    "@riotjs/hot-reload": "^4.0.0",
    "@riotjs/webpack-loader": "^4.0.1",
    "babel-loader": "^8.0.6",
    "riot": "^4.7.1",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  }
}
```

`npm run build`でビルド。

```php:ビルド中のログ
> webpack --mode production --devtool source-map

Hello World!!
code: <app>
  <p>{ props.message }</p>
</app>

options: { tagName: null,
  fragments: null,
  options:
   { template: 'step_count',
     file:
      'c:\\nodeapp\\riotv4-custom-template\\src\\scripts\\app.riot',
     scopedCss: true,
     hot: true },
  source: '<app>\r\n  <p>{ props.message }</p>\r\n</app>\r\n' }
Hash: b56824acee93ca59aa1d
Version: webpack 4.41.2
Time: 1403ms
```

動いていますね！
ファイルパスも`options`から取れるので、コミットするなどファイルを使って何かすることもできそうです。

## ステップカウント

とりあえずステップ数(とは名ばかりの行数)を計測してみます。

```javascript:webpack.config.js
// カスタムテンプレートを登録
compiler.registerPreprocessor('template', 'step_count', (code, options) => {
  console.log(`*** Step Count *** ${ (new String(options.options.file)).replace(/.*\\/, "") }:${ code.split("\n").length }`);
  return {
    code: code
  };
});
```
```php:ビルド中のログ
> webpack --mode production --devtool source-map

*** Step Count *** goodbye.riot:6
*** Step Count *** message.riot:22
*** Step Count *** hello.riot:4
*** Step Count *** app.riot:9
Hash: 6dc6caa7dcd7ec4463ef
Version: webpack 4.41.2
Time: 2757ms
```

## モールス信号HTMLテンプレートエンジン

```javascript:webpack.config.js
const path = require('path');
const compiler = require('@riotjs/compiler');

// モールス信号パターン
const pattern = {
  "・－": "a",
  "－・・・": "b",
  "－・－・": "c",
  "－・・": "d",
  "・": "e",
  "・・－・": "f",
  "－－・": "g",
  "・・・・": "h",
  "・・": "i",
  "・－－－": "j",
  "－・－": "k",
  "・－・・": "l",
  "－－": "m",
  "－・": "n",
  "－－－": "o",
  "・－－・": "p",
  "－－・－": "q",
  "・－・": "r",
  "・・・": "s",
  "－": "t",
  "・・－": "u",
  "・・・－": "v",
  "・－－": "w",
  "－・・－": "x",
  "－・－－": "y",
  "－－・・": "z",
  "・－－－－": "1",
  "・・－－－": "2",
  "・・・－－": "3",
  "・・・・－": "4",
  "・・・・・": "5",
  "－・・・・": "6",
  "－－・・・": "7",
  "－－－・・": "8",
  "－－－－・": "9",
  "－－－－－": "0",
  "－・・・・－": "-"
};

// モールス信号解読
const decode = target => target.split(" ").map(word => pattern[word] || word).join("");

// モールス信号テンプレートを登録
compiler.registerPreprocessor('template', 'morse', code => {
  var reg = /(<\/{0,1})(.+?)(>)/g;
  var match;
  var ret = code;
  while ((match = reg.exec(code)) !== null) {
    ret = ret.replace(match[0], `${ match[1] }${ decode(match[2]) }${ match[3] }`);
  }
  console.log(ret);
  return {
    code: ret
  };
});

module.exports = {
  mode: 'development',
  //mode: 'production',

  entry: './src/scripts/index.js',
  output: {
    path: path.resolve(__dirname, 'app/scripts'),
    filename: 'bundle.js',
    publicPath: '/scripts/',
  },

  devtool: 'inline',
  //devtool: 'source-map',

  module: {
    rules: [
      {
        test: /\.riot$/,
        exclude: /node_modules/,
        use: [{
          loader: '@riotjs/webpack-loader',
          options: {
            hot: true, // set it to true if you are using hmr
            // add here all the other @riotjs/compiler options riot.js.org/compiler
            template: 'morse'
          }
        }]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};
```
```html:app.riot
<・－ ・－－・ ・－－・>
  <・－－・>{ props.message }</・－－・>
</・－ ・－－・ ・－－・>
```
```html:hello.riot
<－－ －・－－ －・・・・－ ・・・・ ・ ・－・・ ・－・・ －－－>
  <・・・・ ・－－－－>Hello World!!</・・・・ ・－－－－>
</－－ －・－－ －・・・・－ ・・・・ ・ ・－・・ ・－・・ －－－>
```
```html:goodbye.riot
<－－ －・－－ －・・・・－ －－・ －－－ －－－ －・・ －・・・ －・－－ ・>
  <・－－・>Goodbye World!!</・－－・>
</－－ －・－－ －・・・・－ －－・ －－－ －－－ －・・ －・・・ －・－－ ・>
```
```html:message.riot
<－－ －・－－ －・・・・－ －－ ・ ・・・ ・・・ ・－ －－・ ・>
  <－・・ ・・ ・・・－>{ props.message.slice(0, 1).toUpperCase() }{ props.message.slice(1) } World!!</－・・ ・・ ・・・－>
</－－ －・－－ －・・・・－ －－ ・ ・・・ ・・・ ・－ －－・ ・>
```

ビルド！

```html:ビルド中のログ
> webpack --mode production --devtool source-map

<my-goodbye>
  <p>Goodbye World!!</p>
</my-goodbye>

<my-message>
  <div>{ props.message.slice(0, 1).toUpperCase() }{ props.message.slice(1) } World!!</div>
</my-message>

<my-hello>
  <h1>Hello World!!</h1>
</my-hello>

<app>
  <p>{ props.message }</p>
</app>

Hash: 8b904bfcedfcd87ff364
Version: webpack 4.41.2
Time: 2239ms
```

モールス信号HTMLテンプレートエンジンここに爆誕！