QiitaのLGTMをSushiにするユーザースクリプト

【2020/10/09】
[QiitaのLGTMをSUSHIにするユーザースタイルシート - Qiita](https://qiita.com/m_fukuchan/items/2c5663c71020ef13e3a0)
ユーザースタイルシートを用いた改良版を作りました。合わせてご覧ください。

# 概要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/95b3c406-6e27-4209-cbd3-8e191bb46b9b.png)

[QiitaのいいねがLGTMに変わりました](https://blog.qiita.com/like-to-lgtm/)から、当然の流れとしてSushiにしました。
記事のLGTMボタンの他、トレンドやマイページ、コメントなどに表示されるLGTMもSushiに置換します。

Greasemonkey/Tampermonkeyなどで実行できるユーザースクリプトとして実装しており、[Greasy Fork](https://greasyfork.org/ja/scripts/397790-qiita-sushi)からインストール可能です。

# 実装

[偉大なる先人](https://github.com/mzyy94/like2sushi)はユーザースタイルシートで実装していましたが、筆者としてはユーザースクリプトのほうが[慣れ](https://qiita.com/m_fukuchan/items/2a0c1f7b45894449ff56)[てる](https://qiita.com/m_fukuchan/items/cd9f75ea5a2f78d3f821)のでそっちで実装することにします。

## アイコンの置換

### SVG要素の置換

新しいLGTMアイコンはSVG要素で描かれていますから、まずはそれら全てを[querySelectorAll()](https://developer.mozilla.org/ja/docs/Web/API/Document/querySelectorAll)で発見し、その中身を[OpenSushi](https://github.com/rdrgn/opensushi)の[tuna.svg](https://github.com/rdrgn/opensushi/blob/master/assets/svg/tuna.svg)で置換します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/3ff1cecc-d49b-1014-d316-04833a2de90e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/fd555a57-1043-8920-7bf8-001051424655.png)

さて、そのためには要素の[innerHTML](https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML)を直接置き換えれば良いのですが…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/eae7bcbc-63d4-de79-de35-c5de567670ae.png)

単にそうしただけだと、SVG要素の[viewBox](https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/viewBox)属性の違いからこんな小さなSushiになってしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/9e972e4d-583f-4cdd-388e-ea4d9da00c97.png)

食べ応えがないので、viewBox属性の値も合わせて置き換えることにしましょう。

それと後でコメント欄など遅延読み込みされる要素への対応をするときに繰り返し何度も置換してしまうのを防ぐため、置き換えが完了したSVG要素にはdata-sushinized属性を追加しておきます。

```javascript
/*
 * OpenSushi
 * (c) remin
 */
const viewBox = '0 0 64 64';
const innerHTML = '<defs><style>.a{fill:#eee;}.b{fill:#bdbdbd;}.c{fill:#d32f2f;}.d{fill:#b71c1c;}</style></defs><title>Sushi</title><path class="a" d="M53.94,44.75l-3.07.77a16,16,0,0,1-12.76-2.21l-14.55-9.7A8,8,0,0,1,20,27V23a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,60,33v4A8,8,0,0,1,53.94,44.75Z"/><path class="b" d="M60,33a8,8,0,0,1-6.06,7.73l-3.07.77A16,16,0,0,1,46,42V41a8,8,0,0,0-3.56-6.66l-14.55-9.7A16,16,0,0,0,20.07,22,8.08,8.08,0,0,0,20,23h0v4a8,8,0,0,0,3.56,6.66l14.55,9.7c.42.28.85.53,1.29.76l.29.15c.43.22.87.42,1.31.6l.25.09q.59.23,1.2.41l.29.09c.45.12.9.23,1.36.31l.37.06c.43.07.86.12,1.29.16l.17,0h0a16,16,0,0,0,4.93-.44l3.07-.77A8,8,0,0,0,60,37V33Z"/><path class="a" d="M37.94,54.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,43.61A8,8,0,0,1,4,37V33a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,44,43v4A8,8,0,0,1,37.94,54.75Z"/><path class="b" d="M37.94,50.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,39.61A8,8,0,0,1,4,33v4a8,8,0,0,0,3.56,6.66l14.55,9.7a16,16,0,0,0,12.76,2.21l3.07-.77A8,8,0,0,0,44,47V43A8,8,0,0,1,37.94,50.75Z"/><path class="c" d="M16,17.76v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39a1.2,1.2,0,0,0-.29-.7C56,24,44,15.07,32.22,13a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,16,17.76Z"/><path class="d" d="M61.24,33.36,46.55,37a1,1,0,0,1-.95-.26C37.85,29,28.26,20.59,16.81,18.32a1,1,0,0,1-.78-.77,1,1,0,0,0,0,.21v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39h0A1,1,0,0,1,61.24,33.36Z"/><path class="c" d="M2,26.58V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22a1,1,0,0,0-.29-.7C39.81,33.61,30,23.9,18.22,21.84a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,2,26.58Z"/><path class="d" d="M47.24,43.19,32.55,46.86a1,1,0,0,1-.95-.26C23.85,38.86,14.26,29.41,2.81,27.15A1,1,0,0,1,2,26.37a1,1,0,0,0,0,.21V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22h0A1,1,0,0,1,47.24,43.19Z"/>';

// LGTMのSVG要素をそれぞれSushiに変更する
const images = node.querySelectorAll('*[class*="like"] svg:not([data-sushinized]), .ItemLink__status svg:not([data-sushinized]), .ms-ItemList_counts svg:not([data-sushinized]), svg[class*="Lgtm"]:not([data-sushinized])');
images.forEach(image => {
    image.setAttribute('viewBox', viewBox);
    image.innerHTML = innerHTML;

    image.dataset.sushinized = "1";
});
```

### 既存のアニメーションの削除

![WebpackerなしでNode.jsベースの最小のJavaScriptビルド環境を設定する「minimum_javascript_on_rails」 - Qiita - Mozilla Firefox 2020-03-13 16-34-07.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/5244ee57-7c77-b8d0-c586-b7d576bf3911.gif)

記事をLGTMするボタンのSVG要素には押したときにチェックマークへ変化するアニメーションが仕込まれていますが、これを無効化します。

ただ経験から言ってこういうのは「アニメーションそのものを削除する」より「アニメーションが発生する側から止める」ほうが楽です。

前項で設定したviewBox属性はアニメーションが発動するごとに元に戻されてしまうことを利用します。
[MutationObserver](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)で属性の変更を監視することができますから、属性に変化があったら直ちに元のSushiに戻すようにしてみましょう。

なお元のSushiに戻すときにも属性は変更されますからMutationObserverが反応します。
そうすると無限再帰呼び出しに陷ってしまうので、
元のSushiに戻している最中はMutationObserverを止めなければならないことに注意しましょう。

```javascript
/*
 * OpenSushi
 * (c) remin
 */
const viewBox = '0 0 64 64';
const innerHTML = '<defs><style>.a{fill:#eee;}.b{fill:#bdbdbd;}.c{fill:#d32f2f;}.d{fill:#b71c1c;}</style></defs><title>Sushi</title><path class="a" d="M53.94,44.75l-3.07.77a16,16,0,0,1-12.76-2.21l-14.55-9.7A8,8,0,0,1,20,27V23a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,60,33v4A8,8,0,0,1,53.94,44.75Z"/><path class="b" d="M60,33a8,8,0,0,1-6.06,7.73l-3.07.77A16,16,0,0,1,46,42V41a8,8,0,0,0-3.56-6.66l-14.55-9.7A16,16,0,0,0,20.07,22,8.08,8.08,0,0,0,20,23h0v4a8,8,0,0,0,3.56,6.66l14.55,9.7c.42.28.85.53,1.29.76l.29.15c.43.22.87.42,1.31.6l.25.09q.59.23,1.2.41l.29.09c.45.12.9.23,1.36.31l.37.06c.43.07.86.12,1.29.16l.17,0h0a16,16,0,0,0,4.93-.44l3.07-.77A8,8,0,0,0,60,37V33Z"/><path class="a" d="M37.94,54.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,43.61A8,8,0,0,1,4,37V33a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,44,43v4A8,8,0,0,1,37.94,54.75Z"/><path class="b" d="M37.94,50.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,39.61A8,8,0,0,1,4,33v4a8,8,0,0,0,3.56,6.66l14.55,9.7a16,16,0,0,0,12.76,2.21l3.07-.77A8,8,0,0,0,44,47V43A8,8,0,0,1,37.94,50.75Z"/><path class="c" d="M16,17.76v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39a1.2,1.2,0,0,0-.29-.7C56,24,44,15.07,32.22,13a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,16,17.76Z"/><path class="d" d="M61.24,33.36,46.55,37a1,1,0,0,1-.95-.26C37.85,29,28.26,20.59,16.81,18.32a1,1,0,0,1-.78-.77,1,1,0,0,0,0,.21v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39h0A1,1,0,0,1,61.24,33.36Z"/><path class="c" d="M2,26.58V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22a1,1,0,0,0-.29-.7C39.81,33.61,30,23.9,18.22,21.84a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,2,26.58Z"/><path class="d" d="M47.24,43.19,32.55,46.86a1,1,0,0,1-.95-.26C23.85,38.86,14.26,29.41,2.81,27.15A1,1,0,0,1,2,26.37a1,1,0,0,0,0,.21V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22h0A1,1,0,0,1,47.24,43.19Z"/>';

// SVG要素の中身をSushiに変更するメソッド
const replaceSvg = svg => {
    svg.setAttribute('viewBox', viewBox);
    svg.innerHTML = innerHTML;
};

// LGTMのSVG要素をそれぞれSushiに変更し、要素の変更を監視する
const images = node.querySelectorAll('*[class*="like"] svg:not([data-sushinized]), .ItemLink__status svg:not([data-sushinized]), .ms-ItemList_counts svg:not([data-sushinized]), svg[class*="Lgtm"]:not([data-sushinized])');
images.forEach(image => {
    // Sushiに変更する
    replaceSvg(image);

    // 属性の変更を監視
    new MutationObserver((records, observer) => {
        // 属性変更時に発動するメソッドの中で属性を変更し無限再帰に陥るので一旦observeを停止する
        observer.disconnect();

        // Sushiに再変更する
        replaceSvg(records[0].target);

        // observeを再開
        observer.observe(records[0].target, {attributes: true});
    }).observe(image, {attributes: true});

    // 何重にも監視するのを防止するため、data-sushinized属性を追加しておく
    image.dataset.sushinized = "1";
});
```

### 新しいアニメーションの追加

しかし何のアニメーションも無くなってしまうと今度はなんだか味気ありません。
そこで @reki さんのアイデアを元に、ボタンをクリックしたらSushiを回転させてみることにします。

![Qiitaの通知欄から各ユーザのプロフィールに移動できるユーザースクリプト - Qiita - Mozilla Firefox 2020-03-13 16-28-00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/3ae6fcb6-6c73-3b33-fa11-c1be703bd6b8.gif)

前項のMutationObserverでボタンがクリックされたことは知れるので、その時SVG要素にアニメーション用のclassを付与し、CSSアニメーションでSushiを回転させます。

```javascript
/*
 * OpenSushi
 * (c) remin
 */
const viewBox = '0 0 64 64';
const innerHTML = '<defs><style>.a{fill:#eee;}.b{fill:#bdbdbd;}.c{fill:#d32f2f;}.d{fill:#b71c1c;}</style></defs><title>Sushi</title><path class="a" d="M53.94,44.75l-3.07.77a16,16,0,0,1-12.76-2.21l-14.55-9.7A8,8,0,0,1,20,27V23a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,60,33v4A8,8,0,0,1,53.94,44.75Z"/><path class="b" d="M60,33a8,8,0,0,1-6.06,7.73l-3.07.77A16,16,0,0,1,46,42V41a8,8,0,0,0-3.56-6.66l-14.55-9.7A16,16,0,0,0,20.07,22,8.08,8.08,0,0,0,20,23h0v4a8,8,0,0,0,3.56,6.66l14.55,9.7c.42.28.85.53,1.29.76l.29.15c.43.22.87.42,1.31.6l.25.09q.59.23,1.2.41l.29.09c.45.12.9.23,1.36.31l.37.06c.43.07.86.12,1.29.16l.17,0h0a16,16,0,0,0,4.93-.44l3.07-.77A8,8,0,0,0,60,37V33Z"/><path class="a" d="M37.94,54.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,43.61A8,8,0,0,1,4,37V33a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,44,43v4A8,8,0,0,1,37.94,54.75Z"/><path class="b" d="M37.94,50.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,39.61A8,8,0,0,1,4,33v4a8,8,0,0,0,3.56,6.66l14.55,9.7a16,16,0,0,0,12.76,2.21l3.07-.77A8,8,0,0,0,44,47V43A8,8,0,0,1,37.94,50.75Z"/><path class="c" d="M16,17.76v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39a1.2,1.2,0,0,0-.29-.7C56,24,44,15.07,32.22,13a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,16,17.76Z"/><path class="d" d="M61.24,33.36,46.55,37a1,1,0,0,1-.95-.26C37.85,29,28.26,20.59,16.81,18.32a1,1,0,0,1-.78-.77,1,1,0,0,0,0,.21v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39h0A1,1,0,0,1,61.24,33.36Z"/><path class="c" d="M2,26.58V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22a1,1,0,0,0-.29-.7C39.81,33.61,30,23.9,18.22,21.84a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,2,26.58Z"/><path class="d" d="M47.24,43.19,32.55,46.86a1,1,0,0,1-.95-.26C23.85,38.86,14.26,29.41,2.81,27.15A1,1,0,0,1,2,26.37a1,1,0,0,0,0,.21V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22h0A1,1,0,0,1,47.24,43.19Z"/>';

// SVG要素の中身をSushiに変更するメソッド
const replaceSvg = svg => {
    svg.setAttribute('viewBox', viewBox);
    svg.innerHTML = innerHTML;
};

// LGTMのSVG要素をそれぞれSushiに変更し、要素の変更を監視する
const images = node.querySelectorAll('*[class*="like"] svg:not([data-sushinized]), .ItemLink__status svg:not([data-sushinized]), .ms-ItemList_counts svg:not([data-sushinized]), svg[class*="Lgtm"]:not([data-sushinized])');
images.forEach(image => {
    // Sushiに変更する
    replaceSvg(image);

    // 属性の変更を監視
    new MutationObserver((records, observer) => {
        // 属性変更時に発動するメソッドの中で属性を変更し無限再帰に陥るので一旦observeを停止する
        observer.disconnect();

        // Sushiに再変更する・Sushiを1回転させる
        replaceSvg(records[0].target);
        records[0].target.classList.add("sushi-go-round");

        // observeを再開
        observer.observe(records[0].target, {attributes: true});
    }).observe(image, {attributes: true});

    // 何重にも監視するのを防止するため、data-sushinized属性を追加しておく
    image.dataset.sushinized = "1";
});

// 回転SushiのCSSアニメーションを定義
const style = document.createElement('style');
style.innerText = '.liked svg.sushi-go-round, svg.sushi-go-round[color="#fff"] { animation: sushi-go-round 1s ease; } @keyframes sushi-go-round { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
document.head.append(style);
```

## 文字列の置換

文字列で書かれてる「LGTM」も「Sushi」に置換していきます。

文字列でLGTMという表記のある要素を見つけられる範囲で[querySelectorAll()](https://developer.mozilla.org/ja/docs/Web/API/Document/querySelectorAll)を用いて発見し、[textContent](https://developer.mozilla.org/ja/docs/Web/API/Node/textContent)を[replace()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/replace)で置換しました。

今のところ置換しているのは[マイページ](https://qiita.com/m_fukuchan)にある「LGTMした記事」、[Organizationsのページ](https://qiita.com/organizations/increments)にある「LGTM」(単位表記)、[通知欄](https://qiita.com/notifications)にある「LGTMしました」、[マイルストーン](https://qiita.com/milestones)にある「🎉10 LGTM達成！」などですが、探せば他にもLGTM表記は見つかりそうです。

```javascript
// 文字列のLGTMをSushiに置換する
const texts = node.querySelectorAll('.userPopularItems_likeUnit, .notification_actionWrapper span.bold:last-of-type, li[role="presentation"] a[href*="like"], .ms-ItemHeader_likedCount, .op-CounterItem_name, *[class*="Label"], .msg-Item_body, *[class*="UserAnalyzeResult__TagStatsTitle"], a.st-Dropdown_item[href*="lgtm"], a.st-Dropdown_item[href*="like"]');
texts.forEach(text => {
    text.childNodes.forEach(child => {
        if (child.nodeType === Node.TEXT_NODE) {
            child.textContent = child.textContent.replace("LGTM", "Sushi");
        }
    });
});
```

## 遅延読み込みされる要素への対応

デフォルトでユーザースクリプトはDOMContentLoadedイベント後に実行されるので、単にそのときに上記の処理を行ったのでは遅延読み込みされる要素中のLGTMをSushiにできません。各要素の遅延読み込み後に処理を行う必要があります。

しかし便利なことに[MutationObserver](https://developer.mozilla.org/ja/docs/Web/API/MutationObserver)は要素の属性の変化だけでなく子要素の変化も監視することができます。

そこで遅延読み込みされる要素を見つけられる範囲で[querySelectorAll()](https://developer.mozilla.org/ja/docs/Web/API/Document/querySelectorAll)を用いて発見し、その子要素の変化(=遅延読み込みの完了)を監視、変化があったらその要素に対して上記の処理を行うことにします。

今のところ遅延読み込みされる要素として見ているのは[記事のコメント欄](https://qiita.com/m_fukuchan/items/0842637ae34d7e5aa004#comments)、[マイルストーン](https://qiita.com/milestones)にある記事一覧、[タグ](https://qiita.com/tags/qiita)ごとの記事一覧などですが、探せば他にも遅延読み込みされている要素は見つかりそうです。

## 完成したコード

```javascript
// ==UserScript==
// @name         Qiita Sushi
// @namespace    https://greasyfork.org/users/432749
// @description  QiitaのLGTMアイコンをSushiアイコンに変更する
// @author       fukuchan
// @match        https://qiita.com/*
// ==/UserScript==

/*
 * OpenSushi
 * (c) remin
 */
const viewBox = '0 0 64 64';
const innerHTML = '<defs><style>.a{fill:#eee;}.b{fill:#bdbdbd;}.c{fill:#d32f2f;}.d{fill:#b71c1c;}</style></defs><title>Sushi</title><path class="a" d="M53.94,44.75l-3.07.77a16,16,0,0,1-12.76-2.21l-14.55-9.7A8,8,0,0,1,20,27V23a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,60,33v4A8,8,0,0,1,53.94,44.75Z"/><path class="b" d="M60,33a8,8,0,0,1-6.06,7.73l-3.07.77A16,16,0,0,1,46,42V41a8,8,0,0,0-3.56-6.66l-14.55-9.7A16,16,0,0,0,20.07,22,8.08,8.08,0,0,0,20,23h0v4a8,8,0,0,0,3.56,6.66l14.55,9.7c.42.28.85.53,1.29.76l.29.15c.43.22.87.42,1.31.6l.25.09q.59.23,1.2.41l.29.09c.45.12.9.23,1.36.31l.37.06c.43.07.86.12,1.29.16l.17,0h0a16,16,0,0,0,4.93-.44l3.07-.77A8,8,0,0,0,60,37V33Z"/><path class="a" d="M37.94,54.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,43.61A8,8,0,0,1,4,37V33a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,44,43v4A8,8,0,0,1,37.94,54.75Z"/><path class="b" d="M37.94,50.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,39.61A8,8,0,0,1,4,33v4a8,8,0,0,0,3.56,6.66l14.55,9.7a16,16,0,0,0,12.76,2.21l3.07-.77A8,8,0,0,0,44,47V43A8,8,0,0,1,37.94,50.75Z"/><path class="c" d="M16,17.76v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39a1.2,1.2,0,0,0-.29-.7C56,24,44,15.07,32.22,13a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,16,17.76Z"/><path class="d" d="M61.24,33.36,46.55,37a1,1,0,0,1-.95-.26C37.85,29,28.26,20.59,16.81,18.32a1,1,0,0,1-.78-.77,1,1,0,0,0,0,.21v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39h0A1,1,0,0,1,61.24,33.36Z"/><path class="c" d="M2,26.58V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22a1,1,0,0,0-.29-.7C39.81,33.61,30,23.9,18.22,21.84a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,2,26.58Z"/><path class="d" d="M47.24,43.19,32.55,46.86a1,1,0,0,1-.95-.26C23.85,38.86,14.26,29.41,2.81,27.15A1,1,0,0,1,2,26.37a1,1,0,0,0,0,.21V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22h0A1,1,0,0,1,47.24,43.19Z"/>';

// SVG要素の中身をSushiに変更するメソッド
const replaceSvg = svg => {
    svg.setAttribute('viewBox', viewBox);
    svg.innerHTML = innerHTML;
};

// SVG要素やテキストのLGTMを発見し、全てSushiに変更するメソッド
const sushinize = node => {
    // LGTMのSVG要素をそれぞれSushiに変更し、要素の変更を監視する
    const images = node.querySelectorAll('*[class*="like"] svg:not([data-sushinized]), .ItemLink__status svg:not([data-sushinized]), .ms-ItemList_counts svg:not([data-sushinized]), svg[class*="Lgtm"]:not([data-sushinized])');
    images.forEach(image => {
        // Sushiに変更する
        replaceSvg(image);

        // 属性の変更を監視
        new MutationObserver((records, observer) => {
            // 属性変更時に発動するメソッドの中で属性を変更し無限再帰に陥るので一旦observeを停止する
            observer.disconnect();

            // Sushiに再変更する・Sushiを1回転させる
            replaceSvg(records[0].target);
            records[0].target.classList.add("sushi-go-round");

            // observeを再開
            observer.observe(records[0].target, {attributes: true});
        }).observe(image, {attributes: true});

        // 何重にも監視するのを防止するため、data-sushinized属性を追加しておく
        image.dataset.sushinized = "1";
    });

    // 文字列のLGTMをSushiに置換する
    const texts = node.querySelectorAll('.userPopularItems_likeUnit, .notification_actionWrapper span.bold:last-of-type, li[role="presentation"] a[href*="like"], .ms-ItemHeader_likedCount, .op-CounterItem_name, *[class*="Label"], .msg-Item_body, *[class*="UserAnalyzeResult__TagStatsTitle"], a.st-Dropdown_item[href*="lgtm"], a.st-Dropdown_item[href*="like"]');
    texts.forEach(text => {
        text.childNodes.forEach(child => {
            if (child.nodeType === Node.TEXT_NODE) {
                child.textContent = child.textContent.replace("LGTM", "Sushi");
            }
        });
    });
};

// 現在読み込まれているdocument中のLGTMを全てSushiに置換
sushinize(document);

// コメントなど遅延読み込みされる動的な要素中のLGTMを読み込まれると同時に全てSushiに置換
const dynamicNodes = document.querySelectorAll('#comments, *[class*="List_view"], div[data-hyperapp-app="Milestones"], div[id*="Snackbar"], div[class*="UserMain__Content"], div[class*="UserAnalyzeResult"]');
dynamicNodes.forEach(node => {
    new MutationObserver((records, observer) => {
        observer.disconnect();
        sushinize(node);
        observer.observe(node, {childList: true});
    }).observe(node, {childList: true});
});

// 回転SushiのCSSアニメーションを定義
const style = document.createElement('style');
style.innerText = '.liked svg.sushi-go-round, svg.sushi-go-round[color="#fff"] { animation: sushi-go-round 1s ease; } @keyframes sushi-go-round { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
document.head.append(style);
```

ソースコードは[Github](https://github.com/ia15076/qiita-sushi)と[Greasy Fork](https://greasyfork.org/ja/scripts/397790-qiita-sushi)でも公開しています。

# スクリーンショット

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/baa26a6e-966e-393f-ee1e-58db532f9bbf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/29030796-18ff-06f5-f01d-8d4a7eeb0433.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/cc508ecb-1f28-0e9f-d2e8-1c3033a43afa.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/6e03e322-d964-c17e-ddb4-7b99f456383e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507909/4172ce81-b72d-cf89-6393-583c7839aab9.png)

# TODO

* まだ見つけられていないLGTMを発見し、いずれは全てのLGTMをSushiにする。

# 偉大な人たち

* [rdrgn/opensushi: Open source sushi icons.](https://github.com/rdrgn/opensushi)
* [mzyy94/like2sushi: ついたーの が に変えられてしまったので に戻すやつ](https://github.com/mzyy94/like2sushi)
* [LGTMを戻す - Qiita](https://qiita.com/sapi_kawahara/items/4e05845e285ac087feb5)