インターポレーションを用いずにcalc関数の中でSCSS関数を書けるようにしてみた

コンポネント指向やアトミックデザインについて試行錯誤していたら、calc関数の中でSCSS関数を多用するようになっていました。[詳細はこちら](https://qiita.com/nontan18/items/d821df27534e90e04476)

ただ、これにはインターポレーションを用いる必要があり、可読性が落ちて手間も増えます。
そこで今回は、webpackを用いることで、`calc(pow(2, 2) * 1px)`のような簡潔な記述ができるようにしてみました。NPMパッケージを公開したのでよかったら、よかったらお使いください。(最後のセクション)

# ①calc関数の中でSCSS関数を呼ぶ方法(通常の方法)
通常、`calc(pow(2,2) * 1px)`のようにSCSS関数をそのままcalc関数の中で呼ぶことはできません。calc関数のなかでSCSS関数を用いる場合は、インターポレーション(`#{}`)を用いて`calc(#{pow(2, 2)} * 1px)`のように記述する必要があります。

```example.scss
@function pow($n, $k) {
  $res: 1;
  @for $_ from 1 through $k {
    $res: $res * $n;
  }
  @return $res;
}

:host {
  width: calc(pow(2, 2) * 1px); // ERROR
  width: calc(#{pow(2, 2)} * 1px); // SUCCESS => calc(4 * 1px) 
}

```

# ②SCSS関数では不可能？
これを簡潔に書くために`mycalc()`のような独自の関数をcalcの代わりに使う方法や、node-sassのcustum functionを用いてcalc関数を上書きする方法を考えましたが、いまいちSCSSの機能では困難そうでした。(何か方法を思いつく方はコメントください)

```sample.scss
@function mycalc(...) {
  @return ...
}

:host {
  width: mycalc(pow(2, 2) * 1px); // => mycalcの引数に渡した時点で計算される
  width: mycalc("pow(2, 2) * 1px"); // => 文字列で渡せば可能かもしれないが美しくない
}
```


# ③SCSSのコンパイル前にcalc内の関数を処理する
そこで、webpackのloaderを追加して、SCSSのコンパイル前にcalcの内側に存在する関数をインターポレーションすることにしました。

```loader.js
const loaderUtils = require('loader-utils');

function detectFunctions(src, i, functions=[]){
	for (const name of functions) {
		if (src.substring(i-name.length, i) == name){
			return name.length;
		}
	}
	
	return -1;
}

function interpolation(s, i, j) {
	return s.slice(0, i) + '#{'  + s.substring(i, j) + '}' + s.slice(j);
}

function functionsResolver(src, functions=[]) {
	const separators = [];

	var re = /[\ \:\,]calc/g;
	while((match = re.exec(src)) != null){
		
		let i = match.index;
		let s, e = null;
		let flag = true;
		
		const stack1 = [];
		const stack2 = [];
		const stack3 = [];

		while((flag || stack1.length != 0) && i < src.length) {
			i ++;
			if (src[i] == '(') {
				
				let len = detectFunctions(src, i, functions);
				if(len != -1 && stack2.length == 0) {
					stack3.push([i, len]);
				}

				stack1.push(i);
				flag = false;

			} else if (src[i] == ')') {
				s = stack1.pop();
				e = i;

				if (stack2.length == 0 && stack3.length > 0 && 
					s == stack3[stack3.length-1][0]
				) {
					let [j, len] = stack3.pop();
					// console.log(`>>>> ${src.substring(j - len, e+1)}`);
					separators.push([j-len, e+1]);
				}

			} else if (src[i] == '{') {
				stack2.push(i);
			} else if (src[i] == '}') {
				stack2.pop();
			} else if (src[i] == ';') {
				break;
			}
		}
	}

	let c = 0;
	for (const [i, j] of separators){
		src = interpolation(src, i+c, j+c);
		c = c + 3;
	}
	return src;

}

module.exports = function(source, map) {
  this.cacheable();
  const options = loaderUtils.getOptions(this);
	const functions = options.functions || [];
	source = functionsResolver(source, options.functions);

  this.callback(null, source, map);
}

```

やっていることは単純で、`functionsResolver(sourceSCSS, ['pow'])`のように呼ぶと、calc関数を探し、その中で呼ばれている関数を`#{}`で囲います。

``` input.scss
:host {
  width: calc(pow(2, 2) * 1px);
}
```

```output.scss
:host {
  width: calc(#{pow(2, 2)} * 1px);
}
```

# ④calc-loaderパッケージ

上記のコードをNPMに公開しましたので、`npm i -D calc-loader`でインストールできます。

```sh
$ npm i -D calc-loader
```

あとは、お使いの`webpack.config.js`に`calc-loader`を追加し、

```webpack.config.js
module.exports = {
  // ......, 
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
        // ..., 
        // SCSS Loaderの最後に追加
          { 
            loader: 'calc-loader', 
            options: {
              functions: ['pow'] // calcの中で使用したい関数をリストで設定
            },
          }
        ],
      }
    ],
  },
}
```

コンパイルしてやるだけです。

```sh
$ webpack
```

これで、最初の例はどちらも正常にコンパイル可能になりました。

```example.scss
@function pow($n, $k) {
  $res: 1;
  @for $_ from 1 through $k {
    $res: $res * $n;
  }
  @return $res;
}

:host {
  width: calc(pow(2, 2) * 1px); // SUCCESS => calc(4 * 1px) 
  width: calc(#{pow(2, 2)} * 1px); // SUCCESS => calc(4 * 1px) 
}

```