羅針盤 技術航海日誌

株式会社羅針盤の技術ブログです

Chrome Extension: GitHub上でWebPをプレビューしたい


この記事は羅針盤 アドベントカレンダー 2024の7日目の記事です。
qiita.com

6日目の記事は できるだけWebPで画像をホスティングしたい、そんな私に捧げる話 - 羅針盤 技術航海日誌 でした。


こんにちは、羅針盤の森川です。
好きなキラーはナースとナイトで、好きなサバイバーはシェリルです。かわいい。

前回はWebP化の話でしたが、今回はGitHubでWebPをプレビューする話になります。

GitHubは2024年12月現在はWebPのプレビューに対応していません。

github.com

なにか宗教的な理由でもあるのでしょうか。

Issue作成画面

まあIssueやPull Requestは最悪いいですよ。 でも大量の差分をチェックするのはさすがにしんどいです。吐きそう。

!!! Binary file not shown !!!

きちーっす。100個近いファイルがあって、100個近い Binary file not shown. が表示されて、レビューとは何か、WebPとはなんだったのかを考えさせられる画面です。

GitHub上でファイル差分のWebPをプレビューする方法

困り果てたので夏休みにChrome Extension作りました。 かわいいやつです。

chromewebstore.google.com

データ抜き取りとかはしてないので安心して使ってください。(たぶん)

本当はGitHub上のPublic Repositoryで公開したいところなんですが、なんかコピペされて情報ぶっこ抜いて個人情報をダークウェブで販売するクローンがいっぱい出てきたら嫌だなあ... と思って現時点のコードの一部をかいつまんで出しておきます。

コード断片

vite.config.ts

ViteでビルドしたいのでなんとなくCRXJS使いました。 使わなくても良いと思います。

npm i @crxjs/vite-plugin@2.0.0-beta.28 -D
import { defineConfig } from "vitest/config";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

// TODO: 型エラー直す
export default defineConfig({
  plugins: [crx({ manifest })],
  rollupOptions: {
    input: {
      background: "src/background.ts",
      content: "src/content.ts",
    },
  },
});

manifest.json

本当は対象のパスをもっと制限したかったのですが(/commits/ 以下とか)、 GitHub上の遷移がHistory APIをいじっているせいかコードが発火しなくなるのでGitHubドメインで有効になるようにしました。

  "content_scripts": [
    {
      "js": ["src/content.ts"],
      "matches": ["https://github.com/*"]
    }
  ],
  "background": {
    "service_worker": "src/background.ts",
    "type": "module"
  },
  "permissions": ["activeTab", "scripting"]

background.ts

アイコンクリックでも発火した方がいいかなと思ったんですが、もしかしたらいらないかもしれない。

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id || 0 },
    files: ["content.ts"],
  });
});

content.ts

(a) URL別でのロジック遷移

HTMLの構造が各ページで異なるので、 現在のURLに合わせて違う関数を実行してます。

function runMain() {
  const url = window.location.href;

  if (url.match(/\/blob\/.*\.webp$/)) {
    handleBlobWebpPage();
  } else if (url.match(/\/files$/)) {
    handleFilesPage();
  } else if (url.match(/\/commits\//)) {
    handleFilesPage();
  } else if (url.match(/\/commit\//)) {
    handleCommitPage();
  }
}

(b) プルリクのdiffページ向けロジック

// diff files page. (e.g. commit diff, pull-request diff)
function handleFilesPage() {[f:id:compasscorp:20241206182736p:plain]
  /* (1) */
  const divList = document.querySelectorAll(
    'div[data-file-type=".webp"][data-details-container-group="file"]',
  );
  
  divList.forEach((div) => {
    /* (2) */
    const webpLinkList = Array.from(
      div
        .querySelector('details-menu[role="menu"]')
        ?.querySelectorAll('a[rel="nofollow"]') ?? [],
    ).filter((a) => a.href.endsWith(".webp"));
    if (!webpLinkList.length) {
      return;
    }

    /* (3) */
    const emptyDiv = div.querySelector("div.empty");
    if (!emptyDiv) {
      return;
    }
    const webpLink = webpLinkList[0];
    if (!_isValidImageDomain(webpLink.href)) {
      return;
    }

    replaceToImageTag(emptyDiv, webpLink.href, "WebP Image");
  });
}

上記コードの (1) では最初の const divList = ... で Binary not shown. を部分をまとめたコンテナdivを取得しています。

divをそれぞれループ回して、

(2) では ... を押下した際の View file のURL(WebPファイルへのリンク)を取得しています。(型は都合上配列にしちゃっていますが、単一データを想定)

(3) では件の Binary file nowe shown. の部分を直接取得していて、これをimgタグに書き換えてます。

_isValidImageDomain(url) は相対パス or HTTP(s)の github.com かどうかを判定してます。

(c) エントリポイント

起動時の処理は、先程の(a)の処理を実行するものになります。 直URL遷移時のために一回(a)を実行して、その後に MutationObserver でURLの遷移を検知して、runMainを実行するようにしてます。 (遷移直後に実行するとDOMが構築される前?に実行されちゃうので雑にsetTimeoutつけてます)

const intervalMs = 1000;


function init() {
  runMain();

  // watch for url change. (e.g. history api)
  let lastUrl = location.href;
  new MutationObserver(() => {
    const currentUrl = location.href;
    if (currentUrl !== lastUrl) {
      lastUrl = currentUrl;
      setTimeout(runMain, intervalMs);
    }
  }).observe(document, { subtree: true, childList: true });
}

window.onload = init;

リポジトリは無いけど、みなさまからのアツイ修正パッチお待ちしております。