🍣

QiitaのLGTMをSUSHIにするユーザースタイルシート

2020/10/09に公開

話題のZennというブログがどんなものか気になっておりましたので、とりあえず使ってみようと先程Qiitaに投稿したのと同様の記事を投稿してみます。Zennへは初投稿です。どうぞよろしくお願いします。


概要

sushinize.png

QiitaのLGTMをまた懲りずにSUSHIにしました。

LGTMボタンの他、トレンドやマイページ、コメントなどに表示されるLGTM、文字列中のLGTMまで記事やコメント以外のありとあらゆるLGTMをSUSHIで置き換えます。

Stylusなどで利用できるユーザースタイルシートとして実装しており、以下のリンクからインストール可能です。

Install directly with Stylus

きっかけ

2020年3月にQiitaの「いいね」が「LGTM」に変わった際に、その話題でいくつかの記事が出ていたことを覚えておられる方はいるでしょうか。

私もそのうちの一つ、QiitaのLGTMをSushiにするユーザースクリプトという記事を書いたのですが、この記事で作ったユーザースクリプトの維持にあたって問題が起きるようになってきました。

Qiitaはそのアップデートに伴って、各ページの構成、要素に付与されるclass属性、遅延読み込みのタイミングなどを徐々に変えてきています。

「徐々に」というのが私にとって困ったところです。
元々人様のサイトの仕様変更に便乗して作ったネタユーザースクリプトですから、アップデートで全く動かなくなった、ということになればもういいかなと思えます。

image.png

しかし実際には少しずつ、今までSushiだったものがLGTMになっていくのです。
したがって画面ではSushiとLGTMが中途半端に混ざりあい、さながらチラシ寿司の様相を呈してしまいます。

酢飯の中に他の具材を混ぜることで独特の食感を生んでいるチラシ寿司。関東では五目寿司と言うらしいですね。
それもそれで美味しくて私は好きですが、統一感が重要なWebページにおいてはなんとなく雑然とした印象を帯びてしまいます。
そこで今までは気が向いたときにDocument.querySelector()で指定するセレクタを新しいものに変えたり、遅延読み込みを待ってLGTMアイコンをSushiに変えるなどしてアップデートへの追従を図っていました。

しかしながらQiitaが「遅延読み込みの終了後にLGTMアイコンを付与」みたいなより高度な実装に移行していくにつれ、こちらの実装は「遅延読み込みの後のアイコン付与の後の◯◯を待ってアイコンを置き換え…」という感じでさらに複雑になっていきます。
さらにQiitaはCSSフレームワーク的なもの(たぶんemotionみたいなやつ)の積極的な使用を進めているようで、今までのuserPopularItems_likeUnitのような意味的なクラス属性は徐々に排除されcss-61ricnのような自動付与らしきクラス名だけが存在するようになってきました。

こうなればアップデートへの追従は極めて困難です。
ユーザースクリプトはQiita側のフロントエンドフレームワーク(React)と同じJavaScriptという土俵で動かざるを得ませんが、どうもそのこと自体に無理が生じているように思えます。

ただ前の記事でも少し触れましたが、思えば偉大なる元ネタの人はユーザースクリプトではなくユーザースタイルシート、すなわちJavaScriptではなくCSSで実装していました。

Webページの各要素がどう表示されるかを司っているのはCSSですから、これを使えばQiitaの処理に関係なく最終的なLGTMの表示をSushiで置き換えることができるはずです。そのようにユーザースタイルシートで実装できないでしょうか?

実装

このユーザースタイルシートに必要な実装は大きく分けて2つです。

  • 文字列として記されるLGTMをSUSHIという文字列で置換する
  • アイコンとして示されるLGTMをSUSHIのアイコンで置換する

文字列の置換

SUSHI用フォントの作成

CSSに触れたことのある人なら知っての通り、CSSで文字列を置き換えることはできません。

もちろんcontentプロパティで要素全体を置き換えることならできますが、例えばユーザーページにある「LGTMした記事」という文字列の「LGTM」の部分だけを置き換えるようなことはできません。

「LGTMした記事」「LGTMした投稿」「◯◯さんがあなたの記事◯◯にLGTMしました」…要素全体を置き換えるしかないとすれば、LGTMという文字列が含まれる要素の全パターンを網羅しなければなりません。現実的に実装は不可能でしょう。

そこでこうします。

image.gif

LGTMをSUSHIに置き換える専用のフォントを作りましょう。

OTFフォーマットのフォントには、このような用途で使えそうな機能が2つあります。

  1. 文脈連鎖依存の置換 … あるパターンの文字列を別の文字列に置き換える機能
  2. 合字 … 複数の文字列を1つの記号で置き換える機能

普通に考えれば1で良さそうですが、FontForgeのチュートリアルによればこの機能はラテン文字では使えないようです。
そこで2の機能を使いましょう。「SUSHI」を1文字の記号として定義し、「L」「G」「T」「M」という文字列を置き換える合字(Ligature)とするのです。

フォントはGlyphr Studioというオープンソースのフォントエディタで作成しました。

image.png

このフォントは「L」「G」「T」「M」を「SUSHI」で置き換えるためだけにあるものなので「SUSHI」以外の文字は使わないのですが、置換前の文字列がフォントに含まれなければそもそも置換処理も走りませんので、M+FONTSのMPLUS1p-Regularを元にアルファベットの「A」から「Z」だけ抜き出し、合字として「SUSHI」を加えたフォントを作ることにします。

「SUSHI」は置き換え元の「L」「G」「T」「M」と横幅を揃えたほうが良いと思い、かなりキツめに字詰めしました。

image.png

そうしてフォントは完成したので、Webフォントの形式であるwoff2に圧縮の上Githubで公開しました。

次にこのフォントを適用するCSSを書きましょう。

SUSHI用フォントの適用

@font-face規則で今回作ったフォント(Sushinize M PLUS 1p)を定義の上、記事のタイトル・内容や投稿フォーム・プレビュー、コメントなどSUSHIにするのが適切ではないと思われる部分以外全てに適用しましょう。

ページ全体への適用ですから、body要素にフォントを指定、それ以外の要素ではunsetにしておいてフォント指定の上書きを防ぐのが良さそうです。

実装は次のようになりました。

    @font-face {
        font-family: "Sushinize M PLUS 1p";
        font-style: normal;
        font-weight: 400;
        src: url("https://cdn.jsdelivr.net/gh/ia15076/sushinizemplus1p@latest/SushinizeMPLUS1p-Regular.woff2") format("woff2");
        unicode-range: U+004c, U+0047, U+0054, U+004D;
    }

    @font-face {
        font-family: "M PLUS 1p";
        font-style: normal;
        font-weight: 400;
        src: url("http://mplus-webfonts.sourceforge.jp/mplus-1p-regular.woff") format("woff");
    }

    * {
        font-family: unset !important;
    }

    body {
        font-family: "Sushinize M PLUS 1p", "M PLUS 1p", "FontAwesome", -apple-system, Segoe UI, Helvetica Neue, Hiragino Kaku Gothic ProN, "メイリオ", meiryo, sans-serif !important;
    }

    input,
    textarea,
    div.it-MdContent,
    a[href*="items"]:not([class*="notification"]),
    a[href*="private"]:not([class*="notification"]),
    *[class*="title"],
    *[class*="article"],
    *[class*="comment"],
    *[class*="draft"],
    *[id*="title"],
    *[id*="article"],
    *[id*="comment"],
    *[id*="draft"] {
        font-family: "FontAwesome", -apple-system, Segoe UI, Helvetica Neue, Hiragino Kaku Gothic ProN, "メイリオ", meiryo, sans-serif !important;
    }

image.png

image.png

実際に適用してみたところ、きちんと置き換えられているようです。

アイコンの置換

LGTMアイコンの削除

次にLGTMのアイコンをSUSHIのアイコンで置換します。まずは既存のアイコンを削除しましょう。

アイコン削除にあたって、本当はLGTMアイコンのpath要素にスタイルを適用する形で実装を行いたかったのですが、後にアイコンを置換するときにそれだと難しい(path要素に背景画像などは指定できない)ために諦めました。
path要素のoutline-widthをすっごく太くして要素全体が塗りつぶされたように見せかけてからmask-imageで抜けないかとかフォント作成より時間をかけて色々考えたんですが残念です。

とはいえ出来なかったものは仕方ないので、pathの親要素であるsvgにスタイルを適用しましょう。LGTMのsvg要素を特定する手段がviewBox属性の大きさしかないのが少し不安ですが、偶然他のアイコンが同じ大きさになることはないと信じます。

実装としてはfill-opacityを0にする、つまり塗る色を透明にすることでアイコンを削除したように見せかけています。

    svg[viewBox="0 0 392.81 429"],
    svg[viewBox="0 0 271.61 199.3"],
    svg[viewBox="0 0 564.81 145.68"],
    svg[viewBox="0 0 640.75 145.68"] {
        fill-opacity: 0;
    }

image.png

LGTMアイコンは様々な箇所にありますがパターンとしては上で指定した4つで全てのようです。記事のLGTMアイコンをはじめ、コメントやトップページやマイルストーンページなど見渡す限り全てのLGTMアイコンを削除することが出来ました。

SUSHIアイコンの追加

削除したLGTMのアイコンがあったところにbackground-imageとしてSUSHIのアイコンを配置します。
今回もOpenSushiを利用しました。

また前回記事へのコメントをきっかけに実装していた、クリックした際にSUSHIが回転する機能もここで再び実装してみます。

    @keyframes sushi-go-round {
        0% {
            transform: rotate(0deg);
        }
        100% {
            transform: rotate(360deg);
        }
    }

    svg[viewBox="0 0 392.81 429"],
    svg[viewBox="0 0 271.61 199.3"],
    svg[viewBox="0 0 564.81 145.68"],
    svg[viewBox="0 0 640.75 145.68"] {
        fill-opacity: 0;
        background-image: url("https://cdn.jsdelivr.net/gh/rdrgn/opensushi@latest/assets/png-256/tuna.png");
        background-position: center;
        background-repeat: no-repeat;
        background-size: contain;
    }

    svg[viewBox="0 0 271.61 199.3"],
    svg[viewBox="0 0 640.75 145.68"] {
        animation: sushi-go-round 1s ease;
    }

image.gif

これでLGTMをSUSHIで置き換えることができました!

最終的に出来上がったコード

ユーザースタイルシートとして必要なメタタグの追加などを行い、最終的なコードは次のようになりました。

Githubでも公開中です。

/* ==UserStyle==
@name           Qiita Sushi
@namespace      github.com/ia15076
@version        0.0.1
@license        MIT
@homepageURL    https://github.com/ia15076/qiita-sushi-css
@supportURL     https://github.com/ia15076/qiita-sushi-css/issues
@updateURL      https://github.com/ia15076/qiita-sushi-css/raw/master/qiita-sushi-css.user.css
==/UserStyle== */

@-moz-document domain("qiita.com") {
    @font-face {
        font-family: "Sushinize M PLUS 1p";
        font-style: normal;
        font-weight: 400;
        src: url("https://cdn.jsdelivr.net/gh/ia15076/sushinizemplus1p@latest/SushinizeMPLUS1p-Regular.woff2") format("woff2");
        unicode-range: U+004c, U+0047, U+0054, U+004D;
    }

    @font-face {
        font-family: "M PLUS 1p";
        font-style: normal;
        font-weight: 400;
        src: url("http://mplus-webfonts.sourceforge.jp/mplus-1p-regular.woff") format("woff");
    }

    @keyframes sushi-go-round {
        0% {
            transform: rotate(0deg);
        }
        100% {
            transform: rotate(360deg);
        }
    }

    * {
        font-family: unset !important;
    }

    body {
        font-family: "Sushinize M PLUS 1p", "M PLUS 1p", "FontAwesome", -apple-system, Segoe UI, Helvetica Neue, Hiragino Kaku Gothic ProN, "メイリオ", meiryo, sans-serif !important;
    }

    input,
    textarea,
    div.it-MdContent,
    a[href*="items"]:not([class*="notification"]),
    a[href*="private"]:not([class*="notification"]),
    *[class*="title"],
    *[class*="article"],
    *[class*="comment"],
    *[class*="draft"],
    *[id*="title"],
    *[id*="article"],
    *[id*="comment"],
    *[id*="draft"] {
        font-family: "FontAwesome", -apple-system, Segoe UI, Helvetica Neue, Hiragino Kaku Gothic ProN, "メイリオ", meiryo, sans-serif !important;
    }

    svg[viewBox="0 0 392.81 429"],
    svg[viewBox="0 0 271.61 199.3"],
    svg[viewBox="0 0 564.81 145.68"],
    svg[viewBox="0 0 640.75 145.68"] {
        fill-opacity: 0;
        background-image: url("https://cdn.jsdelivr.net/gh/rdrgn/opensushi@latest/assets/png-256/tuna.png");
        background-position: center;
        background-repeat: no-repeat;
        background-size: contain;
    }

    svg[viewBox="0 0 271.61 199.3"],
    svg[viewBox="0 0 640.75 145.68"] {
        animation: sushi-go-round 1s ease;
    }
}

スクリーンショット

image.png

image.png

image.png

image.png

参考

Discussion