画像化された絵文字を UserScript で元に戻してみるチャレンジ

<!--
hoo-chan さんから編集リクエストをいただきました。ご指摘ありがとうございます。
コメントアウトで書くのもどうかとは思ったのですが、記事が長くなるのもどうかと思ったのでここにて。
-->

[EmojiOne](https://www.emojione.com) みたいな絵文字を画像に置換してくれるライブラリって便利ですよね。異なる環境でも同じように表示させるっていうのは理にかなっている考え方だと思います。

でも、絵文字はあくまで文字なんだから、そこを画像にしてしまうのはなんとなく癪に障ると思いました。私が勝手に[^webfont]。で、なんとか [Greasemonkey](https://addons.mozilla.org/ja/firefox/addon/greasemonkey/) とかの UserScript で対処できないかなーって思ったので、JavaScript とかミリも知らないくせにコピペプログラミングにチャレンジしました。

使用する拡張機能は [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo)、スクリプトは ECMAScript5、jQuery のバージョンは 3.3.1 でした。

[^webfont]: こういうのやりたいなら個人的には Web フォントで対処すべきだと思うのでいろいろなグループはいろいろ頑張れ。

ヤツは alt にいる
--------------------------------------------------

思いついたのはとある [Mastodon](https://github.com/tootsuite/mastodon) インスタンスでのこと。とりあえず絵文字の入ったトゥートを選択して検証したら、該当部分する img 要素の alt 属性がまんま元の絵文字そのものでした。おっ、これは意外と楽にいけるのでは？

ざっとググってみると、いましたいました、[img 要素を alt 属性のテキストに置換してくれるスクリプト](https://teratail.com/questions/29523)。世の中なんでも先人がいるなあ[^alt]。

というわけでこれをいじいじしてこんな感じに。DOM 要素の生成だけ直感的にわからなかったので[ここ](https://qiita.com/kazu56/items/8e92d08fd1bba0a2d771)を見ながらいじりました。

```javascript:絵文字の再置換[code_1]
$("img[alt][class*=emojione]").each(function() { // alt 属性を持ち class に emojione を持つ img 要素について
    const elem = $(this);
    const altAttr = elem.attr("alt");
    if (!altAttr) {
        return;
    }
    const replaceElem = $("<span></span>", {
        class: "textizedEmoji",
        text: altAttr
    });
    elem.replaceWith(replaceElem);
});
```

これを [jsshell](https://chrome.google.com/webstore/detail/jsshell/kmgmkbicahmbceidoidjbkbpkfogaldh) に流し込んでやると見事再置換に成功。でも Tampermonkey に移植するとなにも起こらず。しばらく🤔になりました[^thinking]が、実行タイミング時点ではトゥートの読み込みとかまったくされてないわけなので当然といえば当然です。それに一度の実行では新しいトゥートに対応できません。つまりこれを DOM の変化ごとに回しつづける必要があります。なにそれ。。ぜんぜんゎかんなぃ。。もぅﾏﾁﾞ無理。ﾏﾘｶしょ。。。ぃま青ｺｳﾗ飛んでった。ごぃのことゎもぅどぉでもぃぃんだって。。

[^alt]: alt 属性は代替文字列ですから、すでに誰かがやってる可能性は十分あると思ったんですよね。こういう役立つあれこれをたくさん残してくれるおかげで私みたいな出来の悪いコピペプログラマが育ってしまう。はい、ちゃんと勉強します。
[^thinking]: 🤔になる、個人的に結構使う表現です。絵文字の中でも顔、emoticon は文末につけて感情を示すのに使うことが多いと思うんですが、私はこうやって文中に使うほうが好きです。だからなおさら「文字」であってほしいんですよね……。

MutationObserver is watching you
--------------------------------------------------

DOM の変化をどうやって捉えるのか、~~そもそも DOM とはなんなのか、~~その答えを確かめるべく我々は Qiita の奥地に向かった。

[見つけた記事](https://qiita.com/daisy1754/items/1958e1d5348914a26f06)によると DOMNodeInserted というものを使う方法と MutationObserver というものを使う方法があるらしい。前者はなにやら非推奨らしいので後者を使うことにします。参考にするのは[ここ](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)。

対象要素となにを監視するのかを指定するらしい。でも、どこになにが出てくるかなんてサイトごとに違う。さて、どうしよう。

私が出した答え: **文書構造本体である body 要素を指定し、その子孫を監視する。**

<!--
ここ、私も body でいいかなあとは思っていたのですが「一応、一応ね！」ということで html にしてました。
お墨付きをいただけましたし喜んで変更。
document.documentElement や document.body に関しては不勉強でした。ありがとうございます。
-->

もっといい方法があるかもしれませんが思いつきませんしこれで。

監視オプションには childList と subtree を指定。これで body の子孫要素（つまり文書すべて）が監視できます。まるでディストピアみたいだ（直喩）。

```javascript:DOM 追加を検知[code_2]
$(function() {
    const observer = new MutationObserver(records => { //オブザーバー、変更があった時に処理を実行
        // [code_1]
    });
    observer.observe(document.body, {
        childList: true, // 対象ノードの子ノード（テキストノードも含む）に対する追加・削除を監視
        subtree: true // 対象ノードとその子孫ノードに対する変更を監視
    }); //監視開始
});
```

試しに実行してみたら見事うまくいきました。

しあげはおかあさん
--------------------------------------------------
これでほぼいい感じなんですが、2 点ほど改善の必要がありました。

まずひとつ。Mastodon にはカスタム絵文字というものがありまして、インスタンスごとに設定された画像をトゥートに組み込める機能があります[^boss]。が、これで表示された画像も絵文字と同じフォーマットなうえ、alt にはカスタム絵文字を表示させるためのショートコードもどきが入っています。つまりこのまま走らせるとせっかく鯖缶が実装してくれた絵文字が実装前と同じ表示になります。これはいけない。

ふたつ。ご存知 [Twitter](https://twitter.com) でも絵文字を画像化する機能があるんですがそのクラス名はちょっと違う[^twitter]。これもなんとかしたい。

そんなわけで[code_1]を以下のように改変。

```javascript:絵文字の再置換改[code_1']
$("img[alt][class*=emoji i]").each(function() { // 大文字小文字を区別せず、class 属性の値に emoji を含む
    const elem = $(this);
    const altAttr = elem.attr("alt");
    if (!altAttr || /:/.test(altAttr)) { // コロンが含まれていた場合も置換を実行しない
        return;
    }
    const replaceElem = $("<span></span>", {
        class: "textizedEmoji",
        text: altAttr
    });
    elem.replaceWith(replaceElem);
});
```

ちなみに、ひとつ目の副産物で絵文字の代わりにショートコードが入ってるタイプ（あなたがまさにいま見ているサイトとか……）での置換を防げるようになりました。うーん、こっちもなんとかしたいけどリスト的ななにか（ふんわり）が必要になりそう。

[^boss]: ところによってはすべてが同じ絵文字だったりする。した。
[^twitter]: たしか「Emoji」。ちなみに公式傘下に入った [TweetDeck](https://tweetdeck.twitter.com) は「emoji」。あのですね。

余談
--------------------------------------------------

### まーた青鳥は余計なことを

ツイート部分のはこれで再置換できるのですが、なんか名前のところとかだけ実装がちょっと違うタイプで私激おこ[^name]。こっちは UserStyle で消し去ってやった。

```CSS:名前欄の絵文字画像を消し、コピペ用？の本来見えない絵文字を表示させる
span.Emoji{ /* 見てのとおり単に消している */
    display: none;
}
span.Emoji + span.visuallyhidden{ /* 消すために行っている処置に対抗呪文 */
    clip: auto;
    position:static;
}
```

### すべてのサイトで実行するには

「すべてのサイトで作用する UserScript」ってあんまり歓迎されてないのか知りませんが、全サイトを指定する方法がざっとググった程度では見つからず、色々試行錯誤する羽目になりました。結論から言うと、以下のようなメタデータブロックを書けばうまくいきました。

```javascript:すべてのサイトで実行するためのメタデータブロック
// ==UserScript==
// @name           Sample Script
// @namespace      http://example.com/
// @version        1.0
// @description    Script Sample
// @include        *://*/*
// ==/UserScript==
```

このワイルドカードらしき「\*」の挙動がよくわからない。0 文字以上の任意の文字パターンってところでしょうか。ちなみにネタバレすると「\*」一文字でも動きます。でもこの方が URL を示していることがわかりやすいので。

[^name]: 名前に＠を挟む感覚で絵文字挟む人結構いますよね。そうでもないか？