Chrome拡張で豊かな暮らし

これはKCS AdventCalendar2021 4日目の記事です。

3日目 | 5日目

こんにちは、fastriver(@fastriver_org)です。先日思いつきで「keiojp自動ログインくん」というChrome拡張を作りました。CanvasLMSのセッションが切れたときに表示されるログイン画面をスキップしてくれる拡張です

この画面を見たくなかったんだ

このような機能はJSでちょちょっと書けば実現できるからみんな作ろう!ということを言いたいのでこの記事を書いています。

Chrome拡張で何ができる?

世に出ている拡張はだいたい以下の3つくらいに分類できると思います。

1.ブラウザの機能を増やす

例) OneTab, ColorZilla, React Developer Tools

これはタブを扱いやすくしたり、カラーピッカーを追加したり、DevToolを拡張したりと特定のサイト云々ではなくブラウザ自体の機能を拡張するものです。作るのが面倒そう

2.特定のサイトを改造する

例) uBlock Origin, Zoom Scheduler, Keepa

これは閲覧するサイトを改造・情報を追加する拡張です。広告を消したり、GoogleカレンダーにZoomのボタンを追加したり、Amazonの商品ページに価格グラフを追加したりと本来のページをいじることで便利にしています。

3.右上のメニューからサービスにアクセスする

例) Google翻訳, LINE

これはサイトやブラウザ自体をいじるのではなく、ブラウザから自サービスを簡単に呼び出せるという機能を持った拡張になります。

なぜChrome拡張…?

なんでChromeだけを?はい、私がChromeを使っているからです。

一応WebExtensions APIという標準仕様があるようで、こちらに準拠するように書けばFirefoxでも使えるようにできると思います。またW3Cでも策定がすすんでいるようなのでSafariでも使えるかもしれません。

しかしブラウザシェアはChromiumが圧倒的というのと、WebExtensions APIについてもChrome拡張を標準化しよう、というものなのでまずChromeのみ対応でいいだろうと考えています(多分標準化の段階で機能が削られる)。

あまり調べず書いているのでWebExtensions APIに詳しい方はツッコミください…

作ってみよう!

思い立ったらとにかく作ってみましょう。必要な知識はJavaScriptの基本構文程度のみです。自分で使うだけなら以下の手順でできます。

  1. フォルダを作る
  2. manifest.jsonを書く
  3. JavaScriptを(ポップアップ出すならHTMLも)書く
  4. Chromeの設定から追加する

ストアに公開したい場合はもう少し手間がかかりますが、野良でも使ってもらうことは可能です。

では実際に作る流れを見ていきます。

manifest.jsonの用意

Chrome拡張はすべてのデータを一つのフォルダにまとめるのでまず適当にフォルダを作ります。

そして設定ファイルとしてmanifest.jsonを作成します。仕様はこちら

現在manifest v2からv3への移行中なので、ネットの情報には気をつける必要があります(古い可能性がある)

{
  "name": "jstage論文参考文献自動生成くん",
  "manifest_version": 3,
  "version": "0.1",
  "author": "fastriver_org",
  "icons": {
    "48": "icon.png"
  },
  "action": null,
  "background": null,
  "permissions": [
]
}

  • name: アプリ名
  • manifest_version: バージョンは3にする
  • version: アプリのバージョン
  • author: 作者
  • icons: 拡張のアイコン。大きさごとに指定する

主な設定は上のようになっています。これらに加えてaction, backgroundにコードを登録、使いたい機能があればpermissionsに追加していく、といった形になります。

Popupを出す

次にPopupを出してみましょう。Popupは右上の拡張アイコンを押したときにでてくる表示のことです。

独立したページのように振る舞うので簡単に実現できます。

Popupを出すにはactionに以下のように追加します。

  "action": {
    "default_title": "論文参考文献自動生成くん",
    "default_popup": "popup.html",
    "default_icon": {
      "48": "icon.png"
    }
  },

記述の通りpopup.htmlをmanifest.jsonと同じディレクトリに置くことでPopupが表示されます。ついでにスクリプト用にpopup.jsも作成してheadに書き足します。

<html>
	<head>
		<meta charset="utf-8" />
		<style>
			body {
				width: 400px;
			}
		</style>
		<script src="popup.js" defer></script>
	</head>
	<body>
		<p id="generated_path"></p>
		<button id="apply">コピーする</button>
	</body>
</html>

ここまでできたらブラウザで拡張を読み込みます。

  1. chrome://extensionsにアクセス
  2. 右上の[デベロッパーモード]を有効にする
  3. [パッケージ化されていない拡張機能を読み込む]からmanifest.jsonのあるフォルダを選択

野良拡張の入れ方

読み込めていれば右上のパズルアイコンから拡張を選択して、Popupを出せるようになっていると思います(ピン留めでバーに表示したままにできる)

最初はアイコンに隠れている

表示されたね

見ているサイトのURLを抽出

表示するだけじゃつまらないので機能を付けましょう。ここでは

  • jstageの論文のPDF画面から直接参考文献の文字列を生成する

ということをやろうと思います。URLの後ろをちょいと変えると論文情報画面に飛べるのでそこから抜き出すことで実現します。

まずは現在表示しているページのURLが必要ですね。これは以下のようにして取得できます。

chrome.tabs.query({ active: true, lastFocusedWindow: true }, async (tabs) => {
  let url = tabs[0].url;
});

現在のタブの情報へのアクセスにはactiveTabの許可が必要なので、manifest.jsonに追加します

  "permissions": [
    "activeTab"
  ]

サイトにアクセスして情報を抜き出す

URLを適当に書き換えてそのURLにGETリクエストします。この辺りは同一生成元ポリシーなどがうるさいですが、Popupは現在のページと同等に扱われるようで特に文句は言われないようです。

以下のコードで元のURLから情報ページのDOMを取得します。

  const parsed = new URL(url);
  const origin = parsed.origin;
  const pathname = parsed.pathname;
  const paths = pathname
    .split("-")[0]
    .split("/")
    .filter((x) => x.length > 0);
  paths.pop();
  const newPath = origin + "/" + paths.join("/") + "/_article/-char/ja";
  //outView.innerText = newPath;
  await fetch(newPath, {
    method: "GET",
  })
    .then(function (response) {
      return response.text();
    })
    .then(function (data) {
      const parser = new DOMParser();
      const doc = parser.parseFromString(data, "text/html");
    });

続いてDOMから必要な情報を抜き出します。実際にそのページの構造をDevToolで見て、当該部のclassなりidなりを見つけます。

雑誌名はclass=jounal-nameなんだな

サイトの構造を変えられると機能不全に陥りますが、そうそう変わらないと信じて進みましょう。classかidが分かればDOMからすぐ取り出せます。

const journalName =
      doc.getElementsByClassName("journal-name")[0]?.innerHTML;

classが複数ある、振られていない等の場合はもう少し工夫が必要になりますが、根気で解決可能です。

情報を整形して表示する

あとは抜き出した情報を適当に整形して表示すれば完成です!popup.jsは以下のようになります。

const outView = document.getElementById("generated_path");
const applyButton = document.getElementById("apply");

const DEFAULT_TEMPLATE = `$authors「$title」($journal, $year 年, $volume 巻 $issue 号, $page)`;

class RefTextGenerator {
  constructor(doc) {
    this.paperTitle =
      doc.getElementsByClassName("global-article-title")[0]?.innerHTML ?? "no";
    this.paperAuthors =
      Array.from(
        doc
          .getElementsByClassName("global-authors-name-tags")[0]
          ?.getElementsByTagName("a")
      ).map((x) => x.innerText) ?? [];
    this.journalName =
      doc.getElementsByClassName("journal-name")[0]?.innerHTML ?? "no";
    const para = doc.getElementsByClassName("global-para")[1]?.innerHTML;
    const ySplitted = para.split("年");
    this.year = ySplitted[0].trim();
    const vSplitted = ySplitted[1].split("巻");
    this.volume = vSplitted[0].trim();
    const iSplitted = vSplitted[1].split("号");
    this.issue = iSplitted[0].trim();
    this.page = iSplitted[1].trim();
  }

  generate(template = DEFAULT_TEMPLATE, authorSeparator = ",") {
    const refText = template
      .replace("$title", this.paperTitle)
      .replace("$authors", this.paperAuthors.join(authorSeparator))
      .replace("$journal", this.journalName)
      .replace("$year", this.year)
      .replace("$volume", this.volume)
      .replace("$issue", this.issue)
      .replace("$page", this.page);
    return refText;
  }
}

chrome.tabs.query({ active: true, lastFocusedWindow: true }, async (tabs) => {
  let url = tabs[0].url;
  const parsed = new URL(url);
  const origin = parsed.origin;
  const pathname = parsed.pathname;
  const paths = pathname
    .split("-")[0]
    .split("/")
    .filter((x) => x.length > 0);
  paths.pop();
  const newPath = origin + "/" + paths.join("/") + "/_article/-char/ja";
  //outView.innerText = newPath;
  await fetch(newPath, {
    method: "GET",
  })
    .then(function (response) {
      return response.text();
    })
    .then(function (data) {
      const parser = new DOMParser();
      const doc = parser.parseFromString(data, "text/html");
      const refTextGenerator = new RefTextGenerator(doc);
      outView.innerText = refTextGenerator.generate(DEFAULT_TEMPLATE);
    });
});

applyButton.addEventListener("click", async () => {
  navigator.clipboard.writeText(outView.innerText);
});

素敵ですね

終わりに

後半のロジック部分は多少煩雑になってしまいましたが、Chrome拡張作成の手軽さ、伝わったでしょうか。皆さんも是非自作の拡張で快適なChromeライフをお過ごしくださいませ。。。

今回サンプルにした拡張は以下で公開しています(テンプレート機能が追加)

https://github.com/organic-nailer/jstage-ref-generator

気に入ったらコントリビュートしてください

Posted on: 2021年12月4日, by :