Riot.js で正規表現テスター

# 概要

Riot.js の学習がてら、正規表現テスターを作ってみました。

![ss.png](https://qiita-image-store.s3.amazonaws.com/0/43328/ce1877a2-0408-0b5b-325a-013e2ec05d32.png)

Regex に正規表現を入れると、Input の文字列のうちマッチした部分が黄色くハイライトされます。

# コード

可読性・保守性の面から、本来は別ファイルでタグを定義した方が良いかと思いますが、本記事ではコピペのしやすさから1ファイルにしています。htmlファイルとして保存し、ブラウザで開くと動作します。
**注意:** コードを見やすくするため、**XSS脆弱性**を残したままにしています。対策は補足説明(2)を読んでください。

```index.html
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <script src='https://cdnjs.cloudflare.com/ajax/libs/riot/2.5.0/riot+compiler.min.js'></script>
  <title>RiotJS Regex Tester</title>
</head>
<body>
  <regex></regex>

  <script type="riot/tag">
    <regex>
      <div class="label">Input:</div><input type="text" onkeyup="{onTextChanged}"><br>
      <div class="label">Regex:</div><input type="text" onkeyup="{onRegexChanged}"><br>
      <regex-result text="{text}" regex="{regex}"></regex-result>
      <style scoped>
        .label {
          width: 3em;
          margin: 5px;
          display: inline-block
        }
      </style>

      this.text = this.regex = '';

      this.onTextChanged = function(e) {
        this.text = e.target.value;
      }.bind(this);

      this.onRegexChanged = function(e) {
        this.regex = e.target.value;
      }.bind(this);
    </regex>
  </script>

  <script type="riot/tag">
    <regex-result>
      <style scoped>
        .marked {
          background-color: yellow;
        }
      </style>

      // Detects the changes of the attributes
      this.on('update', function() {
        // Output string without escaping HTML
        try {
          // CAUTION "XSS vulnerability": opts.text should be escaped.
          this.root.innerHTML = opts.text.replace(new RegExp(opts.regex, 'g'), '<span class="marked">$&</span>');
        }
        catch(e) {
          // Unless try-catch, this app crashes when opts.regex is wrong regular expression.
          console.error(e);
        }
      });
    </regex-result>
  </script>

  <script>
    riot.mount('regex');
  </script>
</body>
</html>
```

# 補足説明

## (1) `<regex-result>`

`<regex-result>` は `<regex>` の子要素です。今回このような構造にしたのは、`<regex-result>` の中でHTMLタグを使用するためです。

入力文と正規表現は属性`text`、`regex`を介して処理されます。`onkeyup`イベントのたびにこれらの値が更新され、その変更に対する処理を `this.on('update', ...` で定義しています。

## (2) HTMLをエスケープせずに出力

`this.root.innerHTML = (HTML)`とすることで、HTMLをエスケープせずに出力できます（`{HTML}`の書き方だとエスケープされます）。

ただし、このままだとXSS脆弱性を残してしまうので、`opts.text`をエスケープする処理が必要です。たとえば次のようにします。(参考: [Escaping HTML strings with jQuery](http://stackoverflow.com/a/12034334))

```
this.root.innerHTML = opts.text.replace(...);
```

↓

```js
let entityMap = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': '&quot;',
  "'": '&#39;',
  "/": '&#x2F;'
};

function escapeHtml(string) {
  return String(string).replace(/[&<>"'\/]/g, function (s) {
    return entityMap[s];
  });
}

let escapedText = escapeHTML(opts.text);
this.root.innerHTML = escapedText.replace(...);
```

---
### （参考）Riot.js のエスケープ処理を使う
@3upjp さんのコメントで、`<regex-result>`を下記のようにする実装を提案していただきました。惜しいことに正規表現に`()`を使うと表示がおかしくなる問題がありますが、学習的な意義が大きいので掲載させていただきます。

```html
  <script type="riot/tag">
    <regex-result>
      <style scoped>
        .marked {
          background-color: yellow;
        }
      </style>
      <span each='{match,i in matches}' class='{marked:i%2}'>{match}</span>
      
      var self = this;
      
      // Detects the changes of the attributes
      this.on('update', function() {
        try {
          self.matches = opts.text.split(new RegExp('(' + opts.regex + ')', 'g'));
        }
        catch(e) {
          // Unless try-catch, this app crashes when opts.regex is wrong regular expression.
          console.error(e);
        }
      });
    </regex-result>
  </script>
```

Riot.js の機能として、「[テンプレート変数](http://riotjs.com/ja/guide/#expressions) > クラス省略記法」「[ループ](http://riotjs.com/ja/guide/#section-23) > 非オブジェクト配列」を使用しています。

また、`split`の引数の正規表現に[キャプチャする括弧](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split#Example:_Capturing_parentheses)を使用すると、返り値の偶数番目がマッチした文字列になることを利用しています。（ただし正規表現が`/(.*?)/`のように全体が`()`で囲まれている場合に限ります。）


# 最後に

Riot.js、スタイルとスクリプトをタグで完結できるのが良いですね。
個人的に WebStorm のプラグイン対応が早く来ればなぁと思っています。