読者です 読者をやめる 読者になる 読者になる

うならぼ

どうも。

Hatena::Let、もしくはES5世代のminifyツール(JavaScript::Squish)でTemplate Literalsを使う

ES2015のTemplate Literals、便利ですねー。プレースホルダの展開といい、改行を含められることといい、"‘をエスケープせずに済むことといい、UserScriptやブックマークレットではとても便利です。

そんな便利な Template Literals ですが、Hatena::Let でそのまま使うと、@javascript_url や .packed.js の出力がまずいことになる場合があります。

どうまずいのか、回避策はないのか、という話をします。

問題点

Hatena::Let では JavaScript::Squish というライブラリでminifyを行っているのですが、これがES2015に対応していません。単にES2015の構文が無視されるだけではありますが、これが Template Literals では問題になります。つまり、文字列リテラルではなくただの式としてminifyされてしまいます。

大きく分けると、「空白が削除されてしまう」「各種開始記号が解釈されてしまう」という二つの問題があります。

1. リテラル内でも空白が削除されてしまう

JavaScript の式において、空白文字はいくつ連続しても意味は変わりません。また、記号で区切られる個所では前後に空白を入れても入れなくてもよいです。例えば次の二つのコードは同義なので、後者にminifyすることが可能です。

return          window   .  alert
  ( 100 -100
    + " === 0"
  );

return window.alert(100-100+" === 0");

というのはJavaScriptに限った話で、例えばCSSを同様にminifyされると意味が変わってしまいます。

body .foo ::before {
  margin: 100px -100px;
}

body.foo::before{margin: 100px-100px;}

あとは Template literals のプレースホルダの直後とか。

${rootid} span { ... }

${rootid}span{...}

JavaScript::Squishでは !%&()*+,-/:;<=>?[]\{|}~ の前後の空白が取り除かれます。

2. リテラル内でも各種開始記号が解釈されてしまう

JavaScriptには正規表現リテラルがあり、スラッシュで始まり、スラッシュ+αで終わります。この中の空白もむやみに取り除くことはできないので、JavaScript::Squish もこれを認識します。ついでに、本来は文字列リテラル正規表現リテラルの中では改行をしてはいけませんが、JavaScript::Squish は緩いので容認します。

let a = `<img src="..." />`;
let b = `<img src="..." />`;
let c = `<img src="..." />`;

これを JavaScript::Squish にかけると次のようになります。

let a=`<img src="..."/>`;
let b = `<img src="..." />`;let c=`<img src="..."/

一行目の />`; から二行目の "..." / までが正規表現リテラルとして認識され、その間はminifyが行われません。三行目の / でやはり正規表現リテラルが始まりますが、このリテラルは終了しないままにソースコードが終了しています。すると、JavaScript::Squish は開始記号より後の文字列を出力しません。

もうめちゃくちゃですね。

回避策

// これを...

alert(`<div class="${LET_ID}" onclick="alert('test')">
</div>`);

// こう。

alert(`'<div class="${LET_ID}" onclick="alert(\'test\')">
</div>'`.slice(1, -1));

すぐ内側にシングルクォートを入れて、ただの文字列リテラルとしても解釈できるようにします。Template literals 内でもエスケープ文字は有効なので、中でシングルクォートを使いたければエスケープします。最後に余分なシングルクォートを取り除いてできあがり。

なお改行が使えているのは JavaScript::Squish が甘いだけで厳密にはダメです。このブログのシンタックスハイライトでも改行を越えては認識していませんね。あとは匿名関数のコメントに書いておいて Function.prototype.toString() してごにょごにょするぐらいですが、minifyで消されるので…一行ずつ書いた文字列リテラルを連結するぐらいでしょうか。

参考

そしてきっかけとなった id:noromamba 氏と id:a-kuma3 氏による作品。誰しも(?)考えるであろう、ページタイトルとURLをまとめてコピーするやつ。

30行でimg要素をダミー画像に置き換える

ダミー画像ごときで外部サービスに依存するのもあれだし、書いた。jQuery使ってるけど使ってないようなもの(?)。メインはcanvasなのでIE9+で動くんじゃないかな。

<img src="" width="160" height="60" alt="バナー">

と書くとこうなる。

f:id:unarist:20170322131041p:plain

いつもながら似たようなことを考える人はいるわけで、細かく設定したいとかSVGできれいに描画したいとかimg要素の属性群を使いたくないとかならそっちの方がよいかと。

Holder.js - client side image placeholders

コード

(function(){
    var canvas = document.createElement('canvas');
    var cache = {};
    $('img[src=""]').each(function(){
        var style = window.getComputedStyle(this);
        var opts = {
            width: this.getAttribute('width'),
            height: this.getAttribute('height'),
            text: this.alt,
            font: style.fontSize + ' ' + style.fontFamily
        };
        var key = JSON.stringify(opts);
        if(!cache[key]) {
            canvas.width = opts.width;
            canvas.height = opts.height;
            var ctx = canvas.getContext('2d');
            
            ctx.fillStyle = '#ccc';
            ctx.fillRect(0, 0, opts.width, opts.height);

            var drawText = '画像: ' + opts.text + ' (' + opts.width + 'x' + opts.height + ')';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.font = opts.font;
            ctx.fillStyle = '#333';
            ctx.fillText(drawText, opts.width / 2, opts.height / 2);
            cache[key] = canvas.toDataURL();
        }
        this.src = cache[key];
    });
})();